VandyHacks/vaken

View on GitHub
src/client/routes/nfc/NfcTable.tsx

Summary

Maintainability
F
1 wk
Test Coverage
F
56%
import React, { useContext, useState, useEffect, FC, useCallback } from 'react';
import { AutoSizer, SortDirection, RowMouseEventHandlerParams } from 'react-virtualized';
import 'react-virtualized/styles.css';
import styled from 'styled-components';
import Select from 'react-select';
import Fuse from 'fuse.js';
import { Button } from '../../components/Buttons/Button';
import { generateRowClassName, createSubmitHandler, CHECK_IN_EVENT_TYPE } from './helpers';

import { ToggleSwitch } from '../../components/Buttons/ToggleSwitch';
import { SearchBox } from '../../components/Input/SearchBox';
import { TableCtxI, TableContext, fuseOpts } from '../../contexts/TableContext';
import {
    useRegisterNfcuidWithUserMutation,
    useCheckInUserToEventMutation,
    useRemoveUserFromEventMutation,
    useCheckInUserToEventByNfcMutation,
    useRemoveUserFromEventByNfcMutation,
} from '../../generated/graphql';

import { NfcTableRows } from './NfcTableRows';

import { QueriedEvent, QueriedHacker, SortFnProps } from './NfcTableTypes';

import STRINGS from '../../assets/strings.json';
import { SmallCenteredText } from '../../components/Text/SmallCenteredText';

const TableLayout = styled('div')`
    width: 100%;
    box-sizing: border-box;
    flex: 1 0 auto;
    display: flex;
    flex-direction: column;
`;

const TableOptions = styled('div')`
    display: flex;
    flex-flow: row wrap;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 1rem;
`;

const EventSelect = styled(Select)`
    min-width: 15rem;
    width: 100%;
    display: inline-block;
    font-size: 1rem;
    margin-right: 1rem;
`;
const UnadmitToggleWrapper = styled('div')`
    margin: 0.5rem 1rem 0.5rem 0;
`;

const ManualToggleWrapper = styled('div')`
    margin: 0.5rem 1rem 0.5rem 0;
`;

// handles basic alphanumeric sorting
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' });

const onSearchBoxEntry = (ctx: TableCtxI): ((e: React.ChangeEvent<HTMLInputElement>) => void) => {
    return e => {
        const { value } = e.target;
        ctx.update(draft => {
            draft.searchValue = value;
        });
    };
};

const onNfcBoxEntry = (ctx: TableCtxI): ((e: React.ChangeEvent<HTMLInputElement>) => void) => {
    return e => {
        const { value } = e.target;
        ctx.update(draft => {
            draft.nfcValue = value;
        });
    };
};

const onSortColumnChange = (ctx: TableCtxI): ((p: SortFnProps) => void) => {
    return ({ sortBy, sortDirection }) => {
        const { sortBy: prevSortBy, sortDirection: prevSortDirection } = ctx.state;
        ctx.update(draft => {
            draft.sortBy =
                // Reset to unordered state if clicked when in decending order
                prevSortBy === sortBy && prevSortDirection === SortDirection.DESC ? undefined : sortBy;
            draft.sortDirection =
                prevSortBy === sortBy && prevSortDirection === SortDirection.DESC
                    ? undefined
                    : sortDirection;
        });
    };
};

const generateOnRowClick = (
    setTopUserMatch: React.Dispatch<React.SetStateAction<string>>
): ((p: RowMouseEventHandlerParams) => void) => ({ rowData }) => {
    if (rowData && rowData.id) setTopUserMatch(rowData.id);
};

export interface Props {
    hackersData: QueriedHacker[];
    eventsData: QueriedEvent[];
}

const NfcTable: FC<Props> = ({ hackersData, eventsData }): JSX.Element => {
    const EventOptions: { label: string; value: string }[] = eventsData.map(event => {
        return {
            label: event.name,
            value: event.id,
        };
    });

    const table = useContext(TableContext);
    const { nfcValue, searchValue, sortBy, sortDirection } = table.state;

    const [sortedData, setSortedData] = useState(hackersData);
    const [topUserMatch, settopUserMatch] = useState('');
    const [manualMode, setManualMode] = useState(false);
    const [unadmitMode, setUnadmitMode] = useState(false);
    const [eventSelected, setEventSelected] = useState(eventsData[0]);
    const onRowClick = useCallback(() => generateOnRowClick(settopUserMatch), [settopUserMatch]);

    const searchBoxRef = React.useRef<HTMLInputElement>(null);

    const [registerNfcUidWithUser] = useRegisterNfcuidWithUserMutation();
    const [checkInUserToEvent] = useCheckInUserToEventMutation();
    const [removeUserFromEvent] = useRemoveUserFromEventMutation();
    const [checkInUserToEventByNfc] = useCheckInUserToEventByNfcMutation();
    const [removeUserFromEventByNfc] = useRemoveUserFromEventByNfcMutation();

    const handleSubmit = createSubmitHandler(
        registerNfcUidWithUser,
        checkInUserToEvent,
        removeUserFromEvent,
        checkInUserToEventByNfc,
        removeUserFromEventByNfc
    );

    useEffect(() => {
        // filter and sort data
        let newData = [...hackersData];

        if (searchValue.trim() !== '') {
            newData = new Fuse(newData, {
                keys: ['email', 'firstName', 'lastName', 'school'] as (keyof QueriedHacker)[],
                ...fuseOpts,
            })
                .search(searchValue)
                .sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
                .slice(0, 5)
                .map(({ item }) => item);
        }

        // Sort data based on props and context
        if (sortBy && sortDirection) {
            newData.sort((a, b) =>
                sortDirection === SortDirection.DESC
                    ? collator.compare(`${b[sortBy]}`, `${a[sortBy]}`)
                    : collator.compare(`${a[sortBy]}`, `${b[sortBy]}`)
            );
        }

        if (searchValue.trim() !== '') {
            if (newData.length) {
                settopUserMatch(newData[0].id);
            }
        } else {
            settopUserMatch('');
        }
        setSortedData(newData);
    }, [hackersData, sortBy, sortDirection, searchValue]);

    return eventsData.length === 0 ? (
        <SmallCenteredText color={STRINGS.DARK_TEXT_COLOR} fontSize="1rem" margin="0rem">
            {STRINGS.NO_EVENTS_MESSAGE}
        </SmallCenteredText>
    ) : (
        <TableLayout>
            <TableOptions>
                <EventSelect
                    width="100%"
                    name="colors"
                    defaultValue={[EventOptions[0]]}
                    options={EventOptions}
                    onChange={(option: { label: string; value: string }) => {
                        const event = eventsData.find(ev => {
                            return ev.id === option.value;
                        });
                        if (event) {
                            setEventSelected(event);
                        }
                    }}
                    className="basic-select"
                    classNamePrefix="select"
                />
                {manualMode || eventSelected.eventType === CHECK_IN_EVENT_TYPE ? (
                    <SearchBox
                        width="100%"
                        value={searchValue}
                        placeholder="Manual Search"
                        onChange={onSearchBoxEntry(table)}
                        ref={searchBoxRef}
                        onKeyPress={async e => {
                            if (manualMode && e.key === 'Enter') {
                                await handleSubmit(nfcValue, topUserMatch, eventSelected, unadmitMode);
                                table.update(draft => {
                                    draft.nfcValue = '';
                                    draft.searchValue = '';
                                });
                                if (searchBoxRef.current) searchBoxRef.current.focus();
                            }
                        }}
                        minWidth="15rem"
                        hasIcon
                        flex
                    />
                ) : null}
                {!manualMode || eventSelected.eventType === CHECK_IN_EVENT_TYPE ? (
                    <SearchBox
                        width="100%"
                        value={nfcValue}
                        placeholder="Scan NFC"
                        onChange={onNfcBoxEntry(table)}
                        onKeyPress={async e => {
                            if (e.key === 'Enter') {
                                await handleSubmit(nfcValue, topUserMatch, eventSelected, unadmitMode);
                                table.update(draft => {
                                    draft.nfcValue = '';
                                    draft.searchValue = '';
                                });
                                if (searchBoxRef.current) searchBoxRef.current.focus();
                            }
                        }}
                        minWidth="15rem"
                        flex
                    />
                ) : null}
                {eventSelected.eventType !== CHECK_IN_EVENT_TYPE ? (
                    <ManualToggleWrapper>
                        <ToggleSwitch
                            label="Manual Mode: "
                            checked={manualMode}
                            onChange={() => {
                                setManualMode(!manualMode);
                            }}
                        />
                    </ManualToggleWrapper>
                ) : null}
                <UnadmitToggleWrapper>
                    <ToggleSwitch
                        label="Unadmit Mode: "
                        checked={unadmitMode}
                        onChange={() => {
                            setUnadmitMode(!unadmitMode);
                        }}
                    />
                </UnadmitToggleWrapper>
                <Button
                    async
                    onClick={() => {
                        const promise = handleSubmit(nfcValue, topUserMatch, eventSelected, unadmitMode);
                        table.update(draft => {
                            draft.nfcValue = '';
                            draft.searchValue = '';
                        });
                        if (searchBoxRef.current) searchBoxRef.current.focus();
                        return promise;
                    }}>
                    Submit
                </Button>
            </TableOptions>
            {manualMode || eventSelected.eventType === CHECK_IN_EVENT_TYPE ? (
                <AutoSizer>
                    {({ height, width }) => (
                        <NfcTableRows
                            width={width}
                            height={height}
                            sortedData={sortedData}
                            onSortColumnChange={onSortColumnChange}
                            generateRowClassName={generateRowClassName(sortedData, topUserMatch)}
                            table={table}
                            rowClickFn={onRowClick}
                        />
                    )}
                </AutoSizer>
            ) : null}
        </TableLayout>
    );
};

export default NfcTable;