index.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import React, { useState, FunctionComponent, useEffect } from 'react';
  2. import Critter, { Time, Month, Colors } from 'types';
  3. import { Table as Tb } from 'reactstrap';
  4. import Tag from 'components/Tag';
  5. import Button from 'components/Button';
  6. import Input from 'components/Input';
  7. import moment from 'moment';
  8. import { parseMonth } from 'services/DataParser';
  9. import { includes, descend, ascend, sortWith } from 'ramda';
  10. import { FaSort, FaSortUp, FaSortDown } from 'react-icons/fa';
  11. import './styles.css';
  12. export type Column<T> = {
  13. label: string,
  14. key: keyof T,
  15. type?: 'time' | 'month',
  16. display?: (critter: T) => string,
  17. sort?: (critter: Record<string, T>) => T
  18. };
  19. enum SortDirection {
  20. Asc,
  21. Desc
  22. };
  23. interface TableProps<T> {
  24. data: T[],
  25. columns: Column<T>[]
  26. };
  27. type Sort<T> = [
  28. SortDirection | undefined,
  29. (critter: Record<string, T>) => T
  30. ]
  31. type Actions<T> = {
  32. search: string,
  33. sort: {
  34. [key: string]: Sort<T>
  35. }
  36. };
  37. const showAvailability = (months: Month[]): { color: Colors, text: string} => {
  38. const currentMonth = (moment().get('month') as Month) + 1;
  39. if (months.length === 12){
  40. return {
  41. color: 'green',
  42. text: 'all year'
  43. }
  44. }
  45. const availableNow = months.findIndex(m => m === currentMonth);
  46. if (availableNow === -1) {
  47. const availableFrom = months.find(m => m > currentMonth) || 1;
  48. return {
  49. color: 'purple',
  50. text: `from ${parseMonth(availableFrom)}`
  51. }
  52. }
  53. const nextIteration = (i: number) => (i + 1) % months.length;
  54. let i = availableNow;
  55. while (true) {
  56. const actual = months[i];
  57. const next = months[nextIteration(i)];
  58. if ( (actual % 12) + 1 !== next) {
  59. const lastMonth = actual === currentMonth;
  60. return lastMonth ? {
  61. color: 'red',
  62. text: 'last month available'
  63. } : {
  64. color: 'orange',
  65. text: `until ${parseMonth(actual)}`
  66. }
  67. } else {
  68. i = nextIteration(i);
  69. }
  70. }
  71. }
  72. const DisplayMonths: FunctionComponent<{months: Month[]}> = ({ months }) => {
  73. const result = showAvailability(months);
  74. return (
  75. <Tag color={result.color}>{result.text}</Tag>
  76. )
  77. }
  78. const DisplayTime: FunctionComponent<{time: Time}> = ({ time }) => {
  79. return (
  80. <div>
  81. {
  82. time.map(([from, to]) => {
  83. return (from === 0 && to === 24) ?
  84. 'All day' : `${from}hs - ${to}hs`
  85. }).join(' & ')
  86. }
  87. </div>
  88. );
  89. }
  90. const DisplayData = (type: 'time' | 'month') => type === 'time' ? DisplayTime : DisplayMonths;
  91. const isInTimeRange = (rangeTime: Time) => {
  92. return rangeTime.some(([from, to]) => moment().isBetween(moment().hour(from).minute(0), moment().hour(to < from ? to + 24 : to).minute(0)));
  93. }
  94. const Table = <T extends Critter>({ data, columns }: TableProps<T>) => {
  95. const [ critters, setState ] = useState<T[]>(data);
  96. const [ actions, setAction ] = useState<Actions<T>>({
  97. search: '',
  98. sort: {}
  99. })
  100. useEffect(() => {
  101. setState(data)
  102. setAction({
  103. search: '',
  104. sort: columns.reduce((_sort, col) => {
  105. if (col.sort) {
  106. return {
  107. ..._sort,
  108. [col.key]: [
  109. undefined,
  110. col.sort
  111. ]
  112. }
  113. }
  114. return _sort
  115. }, {})
  116. })
  117. }, [data, columns]);
  118. const availableNow = () => {
  119. const currentMonth = moment().month() + 1;
  120. const result = critters.filter(critter => {
  121. return includes(currentMonth, critter.months) && isInTimeRange(critter.time)
  122. });
  123. setState(result)
  124. }
  125. const showAll = () => {
  126. setState(data)
  127. setAction({
  128. search: '',
  129. sort: Object.keys(actions.sort).reduce((_sort, key) => {
  130. return {
  131. ..._sort,
  132. [key]: [
  133. undefined,
  134. actions.sort[key][1]
  135. ]
  136. }
  137. }, {})
  138. })
  139. }
  140. const search = () => {
  141. const { search } = actions;
  142. const result = critters.filter(critter => includes(search.toLowerCase(), critter.location.toLowerCase()) || includes(search.toLowerCase(), critter.name.toLowerCase()));
  143. setState(result)
  144. }
  145. const sortData = (_actions: Actions<T>) => {
  146. const sorts = Object.keys(_actions.sort).reduce<((a: Record<string, T>, b: Record<string, T>) => number)[]>((_sort, key) => {
  147. const [ direction, sort] = _actions.sort[key];
  148. if (direction === undefined) {
  149. return _sort
  150. } else {
  151. const x = direction === SortDirection.Asc ? ascend(sort) : descend(sort);
  152. return [
  153. ..._sort,
  154. x
  155. ]
  156. }
  157. }, []);
  158. const _sorted = sortWith(sorts, critters as unknown[] as (Array<Record<string, T>>)) as unknown[] as T[];
  159. setState(_sorted);
  160. }
  161. const setSort = (key: string, sort: Sort<T>) => {
  162. const _actions = {
  163. search: actions.search,
  164. sort: {
  165. ...actions.sort,
  166. [key]: sort
  167. }
  168. }
  169. sortData(_actions);
  170. setAction(_actions);
  171. }
  172. const showSortIcon = (key: string, sort: ((critter: Record<string, T>) => T)) => {
  173. const value = actions.sort[key];
  174. if (value === undefined || value[0] === undefined) {
  175. return (<FaSort onClick={() => {setSort(key, [SortDirection.Asc, sort])}}/>)
  176. } else {
  177. return value[0] === SortDirection.Asc ? (
  178. <FaSortUp onClick={() => {setSort(key, [SortDirection.Desc, sort])}}/>
  179. ) : (
  180. <FaSortDown onClick={() => {setSort(key, [SortDirection.Asc, sort])}}/>
  181. )
  182. }
  183. }
  184. return (
  185. <div className="cc-critter-schedule">
  186. <div className="cc-critter-schedule-actions">
  187. <div className="cc-critter-schedule-actions-search">
  188. <Input value={actions.search} handleChange={(_filter) => { setAction({...actions, search: _filter})}}/>
  189. <Button onClick={() => search()}>search</Button>
  190. </div>
  191. <Button onClick={() => { availableNow() }} color="primary">Available now</Button>
  192. <Button onClick={() => { showAll() }}>Show all</Button>
  193. </div>
  194. <Tb striped hover responsive className="critter-table">
  195. <thead>
  196. <tr>
  197. <th></th>
  198. {
  199. columns.map(({ label, sort, key }, i) => (
  200. <th key={i}>{label} {sort ? showSortIcon(key as string, sort) : null}</th>
  201. ))
  202. }
  203. </tr>
  204. </thead>
  205. <tbody>
  206. {
  207. critters.map((critter, key) => (
  208. <tr key={key}>
  209. <td className="critter-img"><img className="critter-img" src={`${process.env.REACT_APP_API}/${critter.img}`} alt={critter.name}/></td>
  210. {
  211. columns.map(({ key, display, type }, i) => (
  212. <td key={i}>
  213. { type ? DisplayData(type)(critter) :
  214. display ? display(critter) :
  215. critter[key]
  216. }
  217. </td>
  218. ))
  219. }
  220. </tr>
  221. ))
  222. }
  223. </tbody>
  224. </Tb>
  225. </div>
  226. )
  227. }
  228. export default Table;