From 8f087ba35f9ace49d155604904912d448b2370bf Mon Sep 17 00:00:00 2001 From: mhrynko Date: Thu, 11 Aug 2022 16:02:26 +0300 Subject: [PATCH 1/3] add styles and single selection --- src/App.scss | 9 ++++ src/App.tsx | 45 ++++++++-------- src/components/Button.tsx | 15 ++++++ src/components/PeopleTable.tsx | 98 ++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 src/components/Button.tsx create mode 100644 src/components/PeopleTable.tsx 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..d17e8ab2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,35 +4,36 @@ 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'; -export class App extends React.Component { - state = {}; +type State = { + loaded: boolean; +}; + +export class App extends React.Component<{}, State> { + state: Readonly = { + loaded: false, + }; + + componentDidMount() { + setTimeout(() => { + this.setState({ loaded: true }); + }, 500); + } render() { + const { loaded } = this.state; + return (

People table

- - - - - - - - - - - {peopleFromServer.map(person => ( - - - - - - ))} - -
namesexborn
{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..33fd3cf8 --- /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..db629aa6 --- /dev/null +++ b/src/components/PeopleTable.tsx @@ -0,0 +1,98 @@ +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[]; + selectedPerson: Person | null; +}; + +export class PeopleTable extends React.Component<{}, State> { + state: Readonly = { + people: peopleFromServer, + selectedPerson: peopleFromServer[2], + }; + + setPerson = (person: Person | null) => { + this.setState({ selectedPerson: person }); + }; + + render() { + const { people, selectedPerson } = this.state; + + function isSelected(person: Person) { + return person.slug === selectedPerson?.slug; + } + + if (people.length === 0) { + return

No people yet

; + } + + return ( + + + + + + + + + + + + + + {people.map(person => ( + + + + + + + + + ))} + +
+ {selectedPerson?.name || '-'} +
namesexborn
+ {isSelected(person) ? ( + + ) : ( + + )} + + {person.name} + {person.sex}{person.born}
+ ); + } +} From 6ef783253822de457954f942568ced64d678fee7 Mon Sep 17 00:00:00 2001 From: mhrynko Date: Thu, 11 Aug 2022 19:24:34 +0300 Subject: [PATCH 2/3] add multiple selection, reordering and sorting --- src/components/Button.tsx | 2 +- src/components/PeopleTable.tsx | 189 ++++++++++++++++++++++++++++++--- 2 files changed, 176 insertions(+), 15 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 33fd3cf8..655f09a1 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,7 +1,7 @@ import React from 'react'; type Props = React.ButtonHTMLAttributes & { - children: React.ReactNode; + children?: React.ReactNode; }; export const Button: React.FC = ({ children, className, ...props }) => ( diff --git a/src/components/PeopleTable.tsx b/src/components/PeopleTable.tsx index db629aa6..e0d3e6fa 100644 --- a/src/components/PeopleTable.tsx +++ b/src/components/PeopleTable.tsx @@ -7,47 +7,199 @@ import { Button } from './Button'; type State = { people: Person[]; - selectedPerson: Person | null; + selectedPeople: Person[]; + sortField: string; + isReversed: boolean; }; export class PeopleTable extends React.Component<{}, State> { state: Readonly = { people: peopleFromServer, - selectedPerson: peopleFromServer[2], + selectedPeople: [], + sortField: '', + isReversed: false, }; - setPerson = (person: Person | null) => { - this.setState({ selectedPerson: person }); + 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, selectedPerson } = this.state; + const { + people, + selectedPeople, + sortField, + isReversed, + } = this.state; - function isSelected(person: Person) { - return person.slug === selectedPerson?.slug; + 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 ( - - - + + + + + + + - {people.map(person => ( + {visiblePeople.map(person => ( { + ))} From ce404acffb3d33e4ccc0a01c01de960151439156 Mon Sep 17 00:00:00 2001 From: mhrynko Date: Fri, 12 Aug 2022 19:45:52 +0300 Subject: [PATCH 3/3] after hooks --- src/App.tsx | 41 ++++- src/components/PeopleTableHooks.tsx | 236 ++++++++++++++++++++++++++++ 2 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 src/components/PeopleTableHooks.tsx diff --git a/src/App.tsx b/src/App.tsx index d17e8ab2..5ea7cb3e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import '@fortawesome/fontawesome-free/css/all.css'; import 'bulma/css/bulma.css'; @@ -6,12 +6,35 @@ import './App.scss'; import { Loader } from './components/Loader'; import { PeopleTable } from './components/PeopleTable'; +import { PeopleTableHooks } from './components/PeopleTableHooks'; type State = { loaded: boolean; }; -export class App extends React.Component<{}, State> { +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, }; @@ -25,6 +48,20 @@ export class App extends React.Component<{}, State> { 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

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 ( +
- {selectedPerson?.name || '-'} + {selectedPeople.length === 0 ? '-' : ( + <> + {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} +
namesexborn + name + this.sortBy('name')}> + + + + + + sex + this.sortBy('sex')}> + + + + + + born + this.sortBy('born')}> + + + + +
{isSelected(person) ? ( ) : ( {person.sex} {person.born} + + + +
+ + + + + + + + + + + + + + + + + {visiblePeople.map(person => ( + + + + + + + + + + + ))} + +
+

+ {selectedPeople.length === 0 ? '-' : ( + <> + {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} +

+ name + sortBy('name')}> + + + + + + sex + sortBy('sex')}> + + + + + + born + sortBy('born')}> + + + + +
+ {isSelected(person) ? ( + + ) : ( + + )} + + {person.name} + {person.sex}{person.born} + + + +
+ ); +};