diff --git a/src/App.scss b/src/App.scss
index 93f0af3a..ad0ded82 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -2,3 +2,12 @@
iframe {
display: none;
}
+
+button {
+ cursor: pointer; // to that it is clickable
+ transition: 0.3s background-color;
+}
+
+tr {
+ transition: 0.3s background-color;
+}
diff --git a/src/App.tsx b/src/App.tsx
index f6361547..5ea7cb3e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,38 +1,76 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import '@fortawesome/fontawesome-free/css/all.css';
import 'bulma/css/bulma.css';
import './App.scss';
-import peopleFromServer from './people.json';
+import { Loader } from './components/Loader';
+import { PeopleTable } from './components/PeopleTable';
+import { PeopleTableHooks } from './components/PeopleTableHooks';
-export class App extends React.Component {
- state = {};
+type State = {
+ loaded: boolean;
+};
+
+export const App = () => {
+ const [loaded, setLoaded] = useState(false);
+
+ useEffect(() => {
+ setTimeout(() => {
+ setLoaded(true);
+ }, 500);
+ }, []);
+
+ return (
+
+
People table
+
+ {loaded ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export class App2 extends React.Component<{}, State> {
+ state: Readonly = {
+ loaded: false,
+ };
+
+ componentDidMount() {
+ setTimeout(() => {
+ this.setState({ loaded: true });
+ }, 500);
+ }
render() {
+ const { loaded } = this.state;
+
+ /*
+ const useState2 = (initialValue: boolean) => {
+ const { loaded = initialValue } = this.state;
+
+ const setValue = (value: boolean) => {
+ this.setState({ loaded: value });
+ };
+
+ return [loaded, setValue];
+ };
+
+ const [isLoaded, setLoaded] = useState2(false);
+ */
+
return (
People table
-
-
-
- | name |
- sex |
- born |
-
-
-
-
- {peopleFromServer.map(person => (
-
- | {person.name} |
- {person.sex} |
- {person.born} |
-
- ))}
-
-
+ {loaded ? (
+
+ ) : (
+
+ )}
);
}
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
new file mode 100644
index 00000000..655f09a1
--- /dev/null
+++ b/src/components/Button.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+
+type Props = React.ButtonHTMLAttributes & {
+ children?: React.ReactNode;
+};
+
+export const Button: React.FC = ({ children, className, ...props }) => (
+
+);
diff --git a/src/components/PeopleTable.tsx b/src/components/PeopleTable.tsx
new file mode 100644
index 00000000..e0d3e6fa
--- /dev/null
+++ b/src/components/PeopleTable.tsx
@@ -0,0 +1,259 @@
+import classNames from 'classnames';
+import React from 'react';
+
+import peopleFromServer from '../people.json';
+import { Person } from '../types/Person';
+import { Button } from './Button';
+
+type State = {
+ people: Person[];
+ selectedPeople: Person[];
+ sortField: string;
+ isReversed: boolean;
+};
+
+export class PeopleTable extends React.Component<{}, State> {
+ state: Readonly = {
+ people: peopleFromServer,
+ selectedPeople: [],
+ sortField: '',
+ isReversed: false,
+ };
+
+ sortBy = (field: string) => {
+ this.setState(({ isReversed, sortField }) => {
+ const isFirstClick = sortField !== field;
+ const isSecondClick = sortField === field && !isReversed;
+
+ return {
+ sortField: isFirstClick || isSecondClick ? field : '',
+ isReversed: isSecondClick,
+ };
+ });
+ };
+
+ selectPerson = (personToAdd: Person) => {
+ this.setState(state => ({
+ selectedPeople: [...state.selectedPeople, personToAdd],
+ }));
+ };
+
+ unselectPerson = (personToDelete: Person) => {
+ this.setState(state => ({
+ selectedPeople: state.selectedPeople.filter(
+ person => person.slug !== personToDelete.slug,
+ ),
+ }));
+ };
+
+ clearSelection = () => {
+ this.setState({ selectedPeople: [] });
+ };
+
+ moveUp = (personToMove: Person) => {
+ this.setState(({ people }) => {
+ const position = people.findIndex(
+ person => person.slug === personToMove.slug,
+ );
+
+ if (position === 0) {
+ return null;
+ }
+
+ const updatedPeople = [
+ ...people.slice(0, position - 1),
+ people[position],
+ people[position - 1],
+ ...people.slice(position + 1),
+ ];
+
+ return { people: updatedPeople };
+ });
+ };
+
+ moveDown = (personToMove: Person) => {
+ this.setState(({ people }) => {
+ const position = people.findIndex(
+ person => person.slug === personToMove.slug,
+ );
+
+ if (position === people.length - 1) {
+ return null;
+ }
+
+ const updatedPeople = [...people];
+
+ updatedPeople[position] = people[position + 1];
+ updatedPeople[position + 1] = people[position];
+
+ return { people: updatedPeople };
+ });
+ };
+
+ render() {
+ const {
+ people,
+ selectedPeople,
+ sortField,
+ isReversed,
+ } = this.state;
+
+ function isSelected({ slug }: Person) {
+ return selectedPeople.some(person => person.slug === slug);
+ }
+
+ if (people.length === 0) {
+ return No people yet
;
+ }
+
+ const visiblePeople = [...people];
+
+ if (sortField) {
+ visiblePeople.sort(
+ (a, b) => {
+ switch (sortField) {
+ case 'name':
+ case 'sex':
+ return a[sortField].localeCompare(b[sortField]);
+
+ case 'born':
+ return a.born - b.born;
+
+ default:
+ return 0;
+ }
+ },
+ );
+ }
+
+ if (isReversed) {
+ visiblePeople.reverse();
+ }
+
+ return (
+
+
+ {selectedPeople.length === 0 ? '-' : (
+ <>
+ {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
+
+ {selectedPeople.map(p => p.name).join(', ')}
+ >
+ )}
+
+
+
+
+ | |
+
+ name
+ this.sortBy('name')}>
+
+
+
+
+ |
+
+
+ sex
+ this.sortBy('sex')}>
+
+
+
+
+ |
+
+
+ born
+ this.sortBy('born')}>
+
+
+
+
+ |
+
+ |
+
+
+
+
+ {visiblePeople.map(person => (
+
+ |
+ {isSelected(person) ? (
+
+ ) : (
+
+ )}
+ |
+
+
+ {person.name}
+ |
+
+ {person.sex} |
+ {person.born} |
+
+
+
+
+ |
+
+ ))}
+
+
+ );
+ }
+}
diff --git a/src/components/PeopleTableHooks.tsx b/src/components/PeopleTableHooks.tsx
new file mode 100644
index 00000000..6a8e7d6d
--- /dev/null
+++ b/src/components/PeopleTableHooks.tsx
@@ -0,0 +1,236 @@
+import { useState } from 'react';
+import classNames from 'classnames';
+
+import peopleFromServer from '../people.json';
+import { Person } from '../types/Person';
+import { Button } from './Button';
+
+export const PeopleTableHooks = () => {
+ const [people, setPeople] = useState(peopleFromServer);
+ const [selectedPeople, setSelectedPeople] = useState([]);
+ const [sortField, setSortField] = useState('');
+ const [isReversed, setReversed] = useState(false);
+
+ if (people.length === 0) {
+ return No people yet
;
+ }
+
+ // #region methods
+ function isSelected({ slug }: Person) {
+ return selectedPeople.some(person => person.slug === slug);
+ }
+
+ const sortBy = (field: string) => {
+ const isFirstClick = sortField !== field;
+ const isSecondClick = sortField === field && !isReversed;
+
+ setSortField(isFirstClick || isSecondClick ? field : '');
+ setReversed(isSecondClick);
+ };
+
+ const selectPerson = (personToAdd: Person) => {
+ setSelectedPeople(current => [...current, personToAdd]);
+ };
+
+ const unselectPerson = (personToDelete: Person) => {
+ setSelectedPeople(current => current.filter(
+ person => person.slug !== personToDelete.slug,
+ ));
+ };
+
+ const clearSelection = () => {
+ setSelectedPeople([]);
+ };
+
+ const moveUp = (personToMove: Person) => {
+ setPeople(currentPeople => {
+ const position = currentPeople.findIndex(
+ person => person.slug === personToMove.slug,
+ );
+
+ if (position === 0) {
+ return currentPeople;
+ }
+
+ return [
+ ...currentPeople.slice(0, position - 1),
+ currentPeople[position],
+ currentPeople[position - 1],
+ ...currentPeople.slice(position + 1),
+ ];
+ });
+ };
+
+ const moveDown = (personToMove: Person) => {
+ setPeople(curentPeople => {
+ const position = curentPeople.findIndex(
+ person => person.slug === personToMove.slug,
+ );
+
+ if (position === curentPeople.length - 1) {
+ return curentPeople;
+ }
+
+ const updatedPeople = [...curentPeople];
+
+ updatedPeople[position] = curentPeople[position + 1];
+ updatedPeople[position + 1] = curentPeople[position];
+
+ return updatedPeople;
+ });
+ };
+ // #endregion
+
+ // #region visiblePeople
+ const visiblePeople = [...people];
+
+ if (sortField) {
+ visiblePeople.sort(
+ (a, b) => {
+ switch (sortField) {
+ case 'name':
+ case 'sex':
+ return a[sortField].localeCompare(b[sortField]);
+
+ case 'born':
+ return a.born - b.born;
+
+ default:
+ return 0;
+ }
+ },
+ );
+ }
+
+ if (isReversed) {
+ visiblePeople.reverse();
+ }
+ // #endregion
+
+ return (
+
+
+
+ {selectedPeople.length === 0 ? '-' : (
+ <>
+ {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
+
+ {selectedPeople.map(p => p.name).join(', ')}
+ >
+ )}
+
+
+
+
+
+ | |
+
+ name
+ sortBy('name')}>
+
+
+
+
+ |
+
+
+ sex
+ sortBy('sex')}>
+
+
+
+
+ |
+
+
+ born
+ sortBy('born')}>
+
+
+
+
+ |
+
+ |
+
+
+
+
+ {visiblePeople.map(person => (
+
+ |
+ {isSelected(person) ? (
+
+ ) : (
+
+ )}
+ |
+
+
+ {person.name}
+ |
+
+ {person.sex} |
+ {person.born} |
+
+
+
+
+
+ |
+
+ ))}
+
+
+ );
+};