Sfoglia il codice sorgente

Add sort function to Table

Tatiana Inama 6 anni fa
parent
commit
b9b45301b8

+ 16 - 0
app/package-lock.json

@@ -1821,6 +1821,14 @@
         "@types/react": "*"
       }
     },
+    "@types/react-icons": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/react-icons/-/react-icons-3.0.0.tgz",
+      "integrity": "sha512-Vefs6LkLqF61vfV7AiAqls+vpR94q67gunhMueDznG+msAkrYgRxl7gYjNem/kZ+as2l2mNChmF1jRZzzQQtMg==",
+      "requires": {
+        "react-icons": "*"
+      }
+    },
     "@types/react-router": {
       "version": "5.1.5",
       "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.5.tgz",
@@ -11195,6 +11203,14 @@
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz",
       "integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA=="
     },
+    "react-icons": {
+      "version": "3.10.0",
+      "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.10.0.tgz",
+      "integrity": "sha512-WsQ5n1JToG9VixWilSo1bHv842Cj5aZqTGiS3Ud47myF6aK7S/IUY2+dHcBdmkQcCFRuHsJ9OMUI0kTDfjyZXQ==",
+      "requires": {
+        "camelcase": "^5.0.0"
+      }
+    },
     "react-is": {
       "version": "16.13.1",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

+ 2 - 0
app/package.json

@@ -12,12 +12,14 @@
     "@types/ramda": "^0.27.3",
     "@types/react": "^16.9.34",
     "@types/react-dom": "^16.9.6",
+    "@types/react-icons": "^3.0.0",
     "@types/reactstrap": "^8.4.2",
     "bootstrap": "^4.4.1",
     "moment": "^2.24.0",
     "ramda": "^0.27.0",
     "react": "^16.13.1",
     "react-dom": "^16.13.1",
+    "react-icons": "^3.10.0",
     "react-router-dom": "^5.1.2",
     "react-scripts": "3.4.1",
     "reactstrap": "^8.4.1",

+ 7 - 3
app/src/Fishes/index.tsx

@@ -2,6 +2,7 @@ import React, { FunctionComponent, useState, useEffect } from 'react';
 import { getFishes } from 'services/Api';
 import Table, { Column } from 'components/Table';
 import { Fish } from 'types';
+import { prop, path } from 'ramda';
 
 type props = {
 };
@@ -16,11 +17,13 @@ const FishTable: FunctionComponent<props> = () => {
     },
     {
       label: 'price',
-      key: 'price'
+      key: 'price',
+      sort: prop('price')
     },
     {
       label: 'location',
-      key: 'location'
+      key: 'location',
+      sort: prop('location'),
     },
     {
       label: 'size',
@@ -29,7 +32,8 @@ const FishTable: FunctionComponent<props> = () => {
     {
       label: 'time',
       key: 'time',
-      type: "time"
+      type: 'time',
+      sort: path(['time', '0', '0']) as <T>(critter: Record<string, T>) => T
     },
     {
       label: 'availability',

+ 9 - 4
app/src/Insects/index.tsx

@@ -2,6 +2,7 @@ import React, { FunctionComponent, useState, useEffect } from 'react';
 import { Insect } from 'types';
 import { getInsects } from 'services/Api';
 import Table, { Column } from 'components/Table';
+import { prop, path } from 'ramda';
 
 const InsectTable: FunctionComponent = () => {
   const [ data, setData ] = useState<Insect[]>([]);
@@ -12,20 +13,24 @@ const InsectTable: FunctionComponent = () => {
     },
     {
       label: 'price',
-      key: 'price'
+      key: 'price',
+      sort: prop('price')
     },
     {
       label: 'flicks',
-      key: 'flickPrice'
+      key: 'flickPrice',
+      sort: prop('flickPrice')
     },
     {
       label: 'location',
-      key: 'location'
+      key: 'location',
+      sort: prop('location')
     },
     {
       label: 'time',
       key: 'time',
-      type: "time"
+      type: 'time',
+      sort: path(['time', '0', '0']) as (<Insect>(critter: Record<string, Insect>) => Insect)
     },
     {
       label: 'availability',

+ 106 - 35
app/src/components/Table/index.tsx

@@ -6,7 +6,8 @@ import Button from 'components/Button';
 import Input from 'components/Input';
 import moment from 'moment';
 import { parseMonth } from 'services/DataParser';
-import { includes } from 'ramda';
+import { includes, descend, ascend, sortWith } from 'ramda';
+import { FaSort, FaSortUp, FaSortDown } from 'react-icons/fa';
 
 import './styles.css';
 
@@ -14,13 +15,31 @@ export type Column<T> = {
   label: string,
   key: keyof T,
   type?: 'time' | 'month',
-  display?: (critter: T) => string
+  display?: (critter: T) => string,
+  sort?: (critter: Record<string, T>) => T
+};
+
+enum SortDirection {
+  Asc,
+  Desc
 };
 
 interface TableProps<T> {
   data: T[],
   columns: Column<T>[]
-}
+};
+
+type Sort<T> = [
+  SortDirection | undefined,
+  (critter: Record<string, T>) => T
+]
+
+type Actions<T> = {
+  search: string,
+  sort: {
+    [key: string]: Sort<T>
+  }
+};
 
 const showAvailability = (months: Month[]): { color: Colors, text: string} => {
   const currentMonth = (moment().get('month') as Month) + 1;
@@ -89,60 +108,112 @@ const isInTimeRange = (rangeTime: Time) => {
   return rangeTime.some(([from, to]) => moment().isBetween(moment().hour(from).minute(0), moment().hour(to < from ? to + 24 : to).minute(0)));
 }
 
-type State<T> = {
-  critters: T[],
-  relevant: T[],
-  search: string,
-}
 const Table = <T extends Critter>({ data, columns }: TableProps<T>) => {
 
-  const [ state, setState ] = useState<State<T>>({
-    critters: data,
-    relevant: data,
-    search: ''
-  });
+  const [ critters, setState ] = useState<T[]>(data);
+
+  const [ actions, setAction ] = useState<Actions<T>>({
+    search: '',
+    sort: {}
+  })
 
   useEffect(() => {
-    setState({
-      search:'',
-      relevant: data,
-      critters: data,
+    setState(data)
+    setAction({
+      search: '',
+      sort: columns.reduce((_sort, col) => {
+        if (col.sort) {
+          return {
+            ..._sort,
+            [col.key]: [
+              undefined,
+              col.sort
+            ]
+          }
+        }
+        return _sort
+      }, {})
     })
-  }, [data]);
+  }, [data, columns]);
 
   const availableNow = () => {
     const currentMonth = moment().month() + 1;
-    const result = state.critters.filter(critter => {
+    const result = critters.filter(critter => {
       return includes(currentMonth, critter.months) && isInTimeRange(critter.time)
     });
-    setState({
-      ...state,
-      relevant: result
-    })
+    setState(result)
   }
 
   const showAll = () => {
-    setState({
-      ...state,
-      relevant: [...state.critters]
+    setState(data)
+    setAction({
+      search: '',
+      sort: Object.keys(actions.sort).reduce((_sort, key) => {
+        return {
+          ..._sort,
+          [key]: [
+            undefined,
+            actions.sort[key][1]
+          ]
+        }
+      }, {})
     })
   }
 
   const search = () => {
-    const { critters, search } = state;
+    const { search } = actions;
     const result = critters.filter(critter => includes(search.toLowerCase(), critter.location.toLowerCase()) || includes(search.toLowerCase(), critter.name.toLowerCase()));
   
-    setState({
-      ...state,
-      relevant: result
-    })
+    setState(result)
+  }
+
+  const sortData = (_actions: Actions<T>) => {
+     const sorts = Object.keys(_actions.sort).reduce<((a: Record<string, T>, b: Record<string, T>) => number)[]>((_sort, key) => {
+      const [ direction, sort] = _actions.sort[key];
+      if (direction === undefined) {
+        return _sort
+      } else {
+        const x = direction === SortDirection.Asc ? ascend(sort) : descend(sort);
+        return [
+          ..._sort,
+          x
+        ]
+      }
+     }, []);
+     const _sorted = sortWith(sorts, critters as unknown[] as (Array<Record<string, T>>)) as unknown[] as T[];
+     setState(_sorted);
+  }
+
+  const setSort = (key: string, sort: Sort<T>) => {
+    const _actions = {
+      search: actions.search,
+      sort: {
+        ...actions.sort,
+        [key]: sort
+      }
+    }
+    sortData(_actions);
+    setAction(_actions);
+  }
+
+  const showSortIcon = (key: string, sort: ((critter: Record<string, T>) => T)) => {
+    const value = actions.sort[key];
+    if (value === undefined || value[0] === undefined) {
+      return (<FaSort onClick={() => {setSort(key, [SortDirection.Asc, sort])}}/>)
+    } else {
+      return value[0] === SortDirection.Asc ? (
+        <FaSortUp onClick={() => {setSort(key, [SortDirection.Desc, sort])}}/>
+      ) : (
+        <FaSortDown onClick={() => {setSort(key, [SortDirection.Asc, sort])}}/>
+      )
+    }
   }
 
   return (
     <div className="cc-critter-schedule">
       <div className="cc-critter-schedule-actions">
         <div className="cc-critter-schedule-actions-search">
-          <Input value={state.search} handleChange={(_filter) => { setState({...state, search: _filter})}}/>
+          <Input value={actions.search} handleChange={(_filter) => { setAction({...actions, search: _filter})}}/>
           <Button onClick={() => search()}>search</Button>
         </div>
         <Button onClick={() => { availableNow() }} color="primary">Available now</Button>
@@ -153,15 +224,15 @@ const Table = <T extends Critter>({ data, columns }: TableProps<T>) => {
           <tr>
             <th></th>
             {
-              columns.map(({ label }, key) => (
-                <th key={key}>{label}</th>
+              columns.map(({ label, sort, key }, i) => (
+                <th key={i}>{label} {sort ? showSortIcon(key as string, sort) : null}</th>
               ))
             }
           </tr>
         </thead>
         <tbody>
           {
-            state.relevant.map((critter, key) => (
+            critters.map((critter, key) => (
               <tr key={key}>
                 <td className="critter-img"><img className="critter-img" src={`${process.env.REACT_APP_API}/${critter.img}`} alt={critter.name}/></td>
                 {