Skip to content

Commit

Permalink
vinvoor: support adding new cards
Browse files Browse the repository at this point in the history
  • Loading branch information
Topvennie committed Jul 6, 2024
1 parent 52df507 commit f13d543
Show file tree
Hide file tree
Showing 18 changed files with 301 additions and 203 deletions.
2 changes: 1 addition & 1 deletion vingo/handlers/cards.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func StartCardRegisterAPI(c *fiber.Ctx) error {

logger.Println("Card registration started by user", registering_user)

return c.SendStatus(200)
return c.Status(200).JSON(map[string]bool{})
}

func StartCardRegister(c *fiber.Ctx) error {
Expand Down
2 changes: 2 additions & 0 deletions vinvoor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
"@types/react-router-dom": "^5.3.3",
"@types/react-router-hash-link": "^2.4.9",
"js-cookie": "^3.0.5",
"material-ui-confirm": "^3.0.16",
"mdi-material-ui": "^7.9.1",
"notistack": "^3.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.23.1",
Expand Down
2 changes: 0 additions & 2 deletions vinvoor/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,3 @@ export const App = () => {
</>
);
};

// TODO: Add link to the github repo
28 changes: 20 additions & 8 deletions vinvoor/src/cards/Cards.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import { useState } from "react";
import { createContext, Dispatch, SetStateAction, useState } from "react";
import { LoadingSkeleton } from "../components/LoadingSkeleton";
import { useFetch } from "../hooks/useFetch";
import { Card } from "../types/cards";
import { Card, convertCardJSON } from "../types/cards";
import { CardsEmpty } from "./CardsEmpty";
import { CardsTable } from "./CardsTable";

interface CardContextProps {
cards: readonly Card[];
setCards: Dispatch<SetStateAction<readonly Card[]>>;
}

export const CardContext = createContext<CardContextProps>({
cards: [],
setCards: () => {},
});

export const Cards = () => {
const [cards, setCards] = useState<readonly Card[]>([]);
const { loading, error: _ } = useFetch<readonly Card[]>("cards", setCards);
const { loading, error: _ } = useFetch<readonly Card[]>(
"cards",
setCards,
convertCardJSON
);

return (
<LoadingSkeleton loading={loading}>
{!!cards.length ? (
<CardsTable cards={cards} setCards={setCards} />
) : (
<CardsEmpty />
)}
<CardContext.Provider value={{ cards, setCards }}>
{!!cards.length ? <CardsTable /> : <CardsEmpty />}
</CardContext.Provider>
</LoadingSkeleton>
);
};
135 changes: 99 additions & 36 deletions vinvoor/src/cards/CardsAdd.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,110 @@
import { Add, CancelOutlined } from "@mui/icons-material";
import { Add } from "@mui/icons-material";
import { Button, Typography } from "@mui/material";
import { useState } from "react";
import { ConfirmationModal } from "../components/ConfirmationModal";
import { useConfirm } from "material-ui-confirm";
import { useSnackbar } from "notistack";
import { useContext, useState } from "react";
import { Card, CardPostResponse, convertCardJSON } from "../types/cards";
import { getApi, isResponseNot200Error, postApi } from "../util/fetch";
import { equal, randomInt } from "../util/util";
import { CardContext } from "./Cards";

const CHECK_INTERVAL = 1000;
const REGISTER_TIME = 60000;

const confirmTitle = "Register a new card";
const confirmContent = `
Once you click 'register' you will have 60 seconds to hold your card to the scanner.
A popup will appear when the card is registered successfully and it will be added to your cards table.
`;

const requestSuccess = "Register your card by holding it to vinscant";
const requestYou = "You are already registering a card!";
const requestOther =
"Failed to start the card registering process because another user is already registering a card. Please try again later.";
const requestFail =
"Failed to start the card registration process. Please try again later or contact a sysadmin";

const registerSucces = "Card registered successfully";
const registerFail = "Failed to register card";

const getCards = () =>
getApi<readonly Card[]>("cards", convertCardJSON).catch((_) => null);

const checkCardsChange = async (): Promise<
[boolean, readonly Card[] | null]
> => {
const startTime = Date.now();
const cardsStart = await getCards();

if (!cardsStart) return [false, null];

let cardsNow: readonly Card[] | null = null;
while (Date.now() - startTime < REGISTER_TIME) {
cardsNow = await getCards();

if (!equal(cardsStart, cardsNow)) break;

await new Promise((r) => setTimeout(r, CHECK_INTERVAL));
}

return [cardsNow !== null && cardsNow !== cardsStart, cardsNow];
};

export const CardsAdd = () => {
const [open, setOpen] = useState<boolean>(false);
const { setCards } = useContext(CardContext);
const [disabled, setDisabled] = useState<boolean>(false);
const confirm = useConfirm();
const { enqueueSnackbar, closeSnackbar } = useSnackbar();

const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const startRegistering = () =>
postApi<Card[]>("cards/register")
.then(() => {
const id = randomInt().toString();
enqueueSnackbar(requestSuccess, {
variant: "info",
persist: true,
key: id,
});
setDisabled(true);

const title = "Register a new card";
checkCardsChange().then((result) => {
closeSnackbar(id);
setDisabled(false);

const content = `
This feature is not yet implemented as I'm waiting for an endpoint.
Hannes................................................
`;
if (result[0] && result[1] !== null) {
enqueueSnackbar(registerSucces, { variant: "success" });
setCards(result[1]);
} else enqueueSnackbar(registerFail, { variant: "error" });
});
})
.catch((error) => {
if (isResponseNot200Error(error)) {
error.response.json().then((response: CardPostResponse) => {
if (response.is_current_user)
enqueueSnackbar(requestYou, { variant: "warning" });
else
enqueueSnackbar(requestOther, { variant: "error" });
});
} else enqueueSnackbar(requestFail, { variant: "error" });
});

const actions = (
<>
<Button onClick={handleClose} color="error" variant="contained">
<CancelOutlined sx={{ mr: 1 }} />
<Typography>Cancel</Typography>
</Button>
<Button color="success" variant="contained">
<Add sx={{ mr: 1 }} />
<Typography>Register</Typography>
</Button>
</>
);
const handleClick = () => {
confirm({
title: confirmTitle,
description: confirmContent,
confirmationText: "Register",
}).then(() => startRegistering());
};

return (
<>
<Button onClick={handleOpen} variant="contained" sx={{ my: "1%" }}>
<Add />
<Typography>Register new card</Typography>
</Button>
<ConfirmationModal
open={open}
onClose={handleClose}
title={title}
content={content}
actions={actions}
></ConfirmationModal>
</>
<Button
onClick={handleClick}
variant="contained"
sx={{ my: "1%" }}
disabled={disabled}
>
<Add />
<Typography>Register new card</Typography>
</Button>
);
};
56 changes: 17 additions & 39 deletions vinvoor/src/cards/CardsDelete.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
import { CancelOutlined } from "@mui/icons-material";
import DeleteIcon from "@mui/icons-material/Delete";
import { Button, IconButton, Tooltip, Typography } from "@mui/material";
import { Dispatch, FC, SetStateAction, useState } from "react";
import { ConfirmationModal } from "../components/ConfirmationModal";
import { Card } from "../types/cards";
import { IconButton, Tooltip } from "@mui/material";
import { useConfirm } from "material-ui-confirm";
import { FC } from "react";

interface CardDeleteProps {
selected: readonly string[];
setCards: Dispatch<SetStateAction<readonly Card[]>>;
}

export const CardsDelete: FC<CardDeleteProps> = ({ selected, setCards }) => {
const [open, setOpen] = useState<boolean>(false);

export const CardsDelete: FC<CardDeleteProps> = ({ selected }) => {
const confirm = useConfirm();
const numSelected = selected.length;

const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);

const title = `Delete card${numSelected > 1 ? "s" : ""}`;

const content = `
Are you sure you want to delete ${numSelected} card${
numSelected > 1 ? "s" : ""
Expand All @@ -29,33 +21,19 @@ export const CardsDelete: FC<CardDeleteProps> = ({ selected, setCards }) => {
Hannnneeeeeeees...........................
`;

const actions = (
<>
<Button onClick={handleClose} color="error" variant="contained">
<CancelOutlined sx={{ mr: 1 }} />
<Typography>Cancel</Typography>
</Button>
<Button color="warning" variant="contained">
<DeleteIcon sx={{ mr: 1 }} />
<Typography>Delete</Typography>
</Button>
</>
);
const handleClick = () => {
confirm({
title: title,
description: content,
confirmationText: "Delete",
}).then(() => console.log("Card deleted!"));
};

return (
<>
<Tooltip title="Delete">
<IconButton onClick={handleOpen}>
<DeleteIcon />
</IconButton>
</Tooltip>
<ConfirmationModal
open={open}
onClose={handleClose}
title={title}
content={content}
actions={actions}
></ConfirmationModal>
</>
<Tooltip title="Delete">
<IconButton onClick={handleClick}>
<DeleteIcon />
</IconButton>
</Tooltip>
);
};
43 changes: 14 additions & 29 deletions vinvoor/src/cards/CardsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,18 @@
import { Paper, Table, TableContainer, TablePagination } from "@mui/material";
import {
ChangeEvent,
Dispatch,
FC,
MouseEvent,
SetStateAction,
useMemo,
useState,
} from "react";
import { ChangeEvent, MouseEvent, useContext, useMemo, useState } from "react";
import { Card } from "../types/cards";
import { TableOrder } from "../types/table";
import { CardContext } from "./Cards";
import { CardsTableBody } from "./CardsTableBody";
import { CardsTableHead } from "./CardsTableHead";
import { CardsTableToolbar } from "./CardsTableToolbar";

interface CardTableProps {
cards: readonly Card[];
setCards: Dispatch<SetStateAction<readonly Card[]>>;
}

const rowsPerPageOptions = [10, 25, 50];

const descendingComparator = <T,>(a: T, b: T, orderBy: keyof T) => {
if (b[orderBy] < a[orderBy]) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
return 1;
}
if (b[orderBy] < a[orderBy]) return -1;
if (b[orderBy] > a[orderBy]) return 1;

return 0;
};

Expand All @@ -47,18 +32,19 @@ const stableSort = <T,>(
array: readonly T[],
comparator: (a: T, b: T) => number
) => {
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
stabilizedThis.sort((a, b) => {
const stabilized = array.map((el, index) => [el, index] as [T, number]);
stabilized.sort((a, b) => {
const order = comparator(a[0], b[0]);
if (order !== 0) {
return order;
}
return a[1] - b[1];
});
return stabilizedThis.map((el) => el[0]);
return stabilized.map((el) => el[0]);
};

export const CardsTable: FC<CardTableProps> = ({ cards, setCards }) => {
export const CardsTable = () => {
const { cards } = useContext(CardContext);
const [order, setOrder] = useState<TableOrder>("asc");
const [orderBy, setOrderBy] = useState<keyof Card>("serial");
const [selected, setSelected] = useState<readonly string[]>([]);
Expand All @@ -78,6 +64,7 @@ export const CardsTable: FC<CardTableProps> = ({ cards, setCards }) => {
if (event.target.checked) {
const newSelected = cards.map((n) => n.serial);
setSelected(newSelected);

return;
}

Expand Down Expand Up @@ -114,9 +101,7 @@ export const CardsTable: FC<CardTableProps> = ({ cards, setCards }) => {
const handleChangePage = (
_: MouseEvent<HTMLButtonElement> | null,
newPage: number
) => {
setPage(newPage);
};
) => setPage(newPage);

const handleChangeRowsPerPage = (event: ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10));
Expand All @@ -134,12 +119,12 @@ export const CardsTable: FC<CardTableProps> = ({ cards, setCards }) => {
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
),
[order, orderBy, page, rowsPerPage]
[cards, order, orderBy, page, rowsPerPage]
);

return (
<Paper elevation={4} sx={{ width: "100%", mb: 2 }}>
<CardsTableToolbar selected={selected} setCards={setCards} />
<CardsTableToolbar selected={selected} />
<TableContainer>
<Table>
<CardsTableHead
Expand Down
Loading

0 comments on commit f13d543

Please sign in to comment.