Skip to content

Commit

Permalink
vinvoor: support new card features
Browse files Browse the repository at this point in the history
  • Loading branch information
Topvennie committed Jul 17, 2024
1 parent 3735c44 commit 1864bdc
Show file tree
Hide file tree
Showing 12 changed files with 287 additions and 95 deletions.
4 changes: 2 additions & 2 deletions vingo/handlers/cards.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func CardRegisterStatus(c *fiber.Ctx) error {
is_current_user := registering_user == user.Id
time_remaining := time.Until(registering_end).Seconds()
time_percentage := time_remaining / register_timeout.Seconds()
return c.JSON(map[string]interface{}{"registering": register_ongoing, "isCurrentUser": is_current_user, "success": registering_success, "time_remaining": time_remaining, "time_percentage": time_percentage})
return c.JSON(map[string]interface{}{"registering": register_ongoing, "isCurrentUser": is_current_user, "success": registering_success, "timeRemaining": time_remaining, "timePercentage": time_percentage})
}

func CardNameUpdate(c *fiber.Ctx) error {
Expand All @@ -66,5 +66,5 @@ func CardNameUpdate(c *fiber.Ctx) error {
return c.Status(500).SendString("Error updating card name")
}

return c.SendString("Card name updated")
return c.Status(200).JSON(map[string]bool{})
}
163 changes: 103 additions & 60 deletions vinvoor/src/cards/CardsAdd.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,29 @@ import { Add } from "@mui/icons-material";
import { Button, Typography } from "@mui/material";
import { useConfirm } from "material-ui-confirm";
import { useSnackbar } from "notistack";
import { useContext, useState } from "react";
import { Card, CardPostResponse, convertCardJSON } from "../types/cards";
import { useContext, useEffect, useState } from "react";
import {
Card,
CardGetRegisterResponse,
CardPostResponse,
convertCardJSON,
} from "../types/cards";
import { getApi, isResponseNot200Error, postApi } from "../util/fetch";
import { equal, randomInt } from "../util/util";
import { randomInt } from "../util/util";
import { CardContext } from "./Cards";
import {
CircularTimeProgress,
CircularTimeProgressProps,
} from "./CircularTimeProgress";

const CHECK_INTERVAL = 1000;
const REGISTER_TIME = 60000;
const REGISTER_ENDPOINT = "cards/register";

const defaultProgressProps: CircularTimeProgressProps = {
time: REGISTER_TIME,
percentage: 1,
};

const confirmTitle = "Register a new card";
const confirmContent = `
Expand All @@ -27,88 +42,116 @@ const requestFail =
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 && !equal(cardsNow, cardsStart), cardsNow];
};

export const CardsAdd = () => {
const { setCards } = useContext(CardContext);
const [disabled, setDisabled] = useState<boolean>(false);
const [registering, setRegistering] = useState<boolean>(false);
const [progressProps, setProgressProps] =
useState<CircularTimeProgressProps>(defaultProgressProps);
const confirm = useConfirm();
const { enqueueSnackbar, closeSnackbar } = useSnackbar();

const startRegistering = () =>
postApi<Card[]>("cards/register")
.then(() => {
const id = randomInt().toString();
enqueueSnackbar(requestSuccess, {
variant: "info",
persist: true,
key: id,
});
setDisabled(true);
const checkCardsChange = async (): Promise<boolean> => {
let status: CardGetRegisterResponse =
await getApi<CardGetRegisterResponse>(REGISTER_ENDPOINT);
while (status.registering && status.isCurrentUser) {
setProgressProps({
time: status.timeRemaining,
percentage: status.timePercentage,
});
status = await getApi<CardGetRegisterResponse>(REGISTER_ENDPOINT);
await new Promise((r) => setTimeout(r, CHECK_INTERVAL));
}

checkCardsChange().then((result) => {
closeSnackbar(id);
setDisabled(false);
return status.success;
};

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.isCurrentUser)
enqueueSnackbar(requestYou, { variant: "warning" });
else
enqueueSnackbar(requestOther, { variant: "error" });
const handleRegister = (start: boolean) => {
getApi<CardGetRegisterResponse>(REGISTER_ENDPOINT)
.then(async (response) => {
let started = false;
if (!response.registering && start) {
await postApi<CardPostResponse>(REGISTER_ENDPOINT)
.then(() => (started = true))
.catch((error) => {
if (isResponseNot200Error(error))
error.response
.json()
.then((response: CardPostResponse) => {
if (response.isCurrentUser)
enqueueSnackbar(requestYou, {
variant: "warning",
});
else
enqueueSnackbar(requestOther, {
variant: "error",
});
});
else throw new Error(error);
});
}

if (response.registering && response.isCurrentUser)
started = true;

if (started) {
setRegistering(true);
const id = randomInt().toString();
enqueueSnackbar(requestSuccess, {
variant: "info",
persist: true,
key: id,
});
} else enqueueSnackbar(requestFail, { variant: "error" });
});

checkCardsChange()
.then((scanned) => {
closeSnackbar(id);
setRegistering(false);
if (scanned) {
enqueueSnackbar(registerSucces, {
variant: "success",
});
getApi<readonly Card[]>(
"cards",
convertCardJSON
).then((cards) => setCards(cards));
} else
enqueueSnackbar(registerFail, {
variant: "error",
});
})
.finally(() => setProgressProps(defaultProgressProps));
}
})
.catch(() => enqueueSnackbar(requestFail, { variant: "error" }));
};

const handleClick = () => {
confirm({
title: confirmTitle,
description: confirmContent,
confirmationText: "Register",
})
.then(() => startRegistering())
.then(() => handleRegister(true))
.catch(() => {}); // Required otherwise the confirm dialog will throw an error in the console
};

useEffect(() => {
handleRegister(false);
}, []);

return (
<Button
onClick={handleClick}
variant="contained"
sx={{ my: "1%" }}
disabled={disabled}
disabled={registering}
>
<Add />
{registering ? (
<CircularTimeProgress {...progressProps} />
) : (
<Add />
)}
<Typography>Register new card</Typography>
</Button>
);
};

// TODO: Make plus sign a spinner when registering
18 changes: 9 additions & 9 deletions vinvoor/src/cards/CardsDelete.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import DeleteIcon from "@mui/icons-material/Delete";
import { IconButton, Tooltip } from "@mui/material";
import { IconButton, Link, Tooltip, Typography } from "@mui/material";
import { useConfirm } from "material-ui-confirm";
import { FC } from "react";

Expand All @@ -12,14 +12,14 @@ export const CardsDelete: FC<CardDeleteProps> = ({ selected }) => {
const numSelected = selected.length;

const title = `Delete card${numSelected > 1 ? "s" : ""}`;
const content = `
Are you sure you want to delete ${numSelected} card${
numSelected > 1 ? "s" : ""
}? Unfortunately, this
feature isn't implemented yet. Again, I'm waiting
for an endpoint.
Hannnneeeeeeees...........................
`;
const content = (
<Typography>
` Are you sure you want to delete ${numSelected} card$
{numSelected > 1 ? "s" : ""}? Unfortunately, this feature isn't
available yet. Let's convince Hannes to add this feature by signing
this <Link href="https://chng.it/nQ6GSXVRMJ">petition!</Link>`
</Typography>
);

const handleClick = () => {
confirm({
Expand Down
4 changes: 2 additions & 2 deletions vinvoor/src/cards/CardsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Paper, Table, TableContainer, TablePagination } from "@mui/material";
import { ChangeEvent, MouseEvent, useContext, useMemo, useState } from "react";
import { Card } from "../types/cards";
import { TableOrder } from "../types/table";
import { TableOrder } from "../types/general";
import { CardContext } from "./Cards";
import { CardsTableBody } from "./CardsTableBody";
import { CardsTableHead } from "./CardsTableHead";
Expand Down Expand Up @@ -72,7 +72,7 @@ export const CardsTable = () => {
};

const handleRowClick = (
_: MouseEvent<HTMLTableRowElement>,
_: MouseEvent<HTMLTableCellElement>,
serial: string
) => {
const selectedIndex = selected.indexOf(serial);
Expand Down
80 changes: 69 additions & 11 deletions vinvoor/src/cards/CardsTableBody.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,98 @@
import { EditOutlined } from "@mui/icons-material";
import {
Checkbox,
IconButton,
TableBody,
TableCell,
TableRow,
TextField,
Typography,
} from "@mui/material";
import { FC, MouseEvent } from "react";
import { Card, CardsHeadCells } from "../types/cards";
import { useConfirm } from "material-ui-confirm";
import { useSnackbar } from "notistack";
import { ChangeEvent, FC, MouseEvent, useContext } from "react";
import { Card, CardsHeadCells, convertCardJSON } from "../types/cards";
import { getApi, patchApi } from "../util/fetch";
import { CardContext } from "./Cards";

interface CardsTableBodyProps {
rows: readonly Card[];
isRowSelected: (serial: string) => boolean;
handleClick: (
event: MouseEvent<HTMLTableRowElement>,
event: MouseEvent<HTMLTableCellElement>,
serial: string
) => void;
emptyRows: number;
}

const nameSaveSuccess = "New name saved successfully";
const nameSaveFailure = "Unable to save new name";

export const CardsTableBody: FC<CardsTableBodyProps> = ({
rows,
isRowSelected,
handleClick,
emptyRows,
}) => {
const { setCards } = useContext(CardContext);
const confirm = useConfirm();
const { enqueueSnackbar } = useSnackbar();

const handleEditClick = (id: number, name: string) => {
let newName = name;
confirm({
title: "Enter new name",
content: (
<TextField
variant="standard"
defaultValue={name}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
(newName = event.target.value)
}
></TextField>
),
confirmationText: "Save",
})
.then(() => {
if (newName === name) {
enqueueSnackbar(nameSaveSuccess, { variant: "success" });
return;
}

patchApi(`cards/${id}`, { name: newName })
.then(() => {
enqueueSnackbar(nameSaveSuccess, {
variant: "success",
});
getApi<readonly Card[]>("cards", convertCardJSON).then(
(cards) => setCards(cards)
);
})
.catch((error) => {
enqueueSnackbar(nameSaveFailure, { variant: "error" });
console.log(error);
});
})
.catch(() => {}); // Required otherwise the confirm dialog will throw an error in the console
};

const editButton = (id: number, name: string) => (
<IconButton onClick={() => handleEditClick(id, name)}>
<EditOutlined />
</IconButton>
);

return (
<TableBody>
{rows.map((row) => {
const isSelected = isRowSelected(row.serial);

return (
<TableRow
key={row.serial}
selected={isSelected}
onClick={(event) => handleClick(event, row.serial)}
sx={{ cursor: "pointer" }}
>
<TableCell padding="checkbox">
<TableRow key={row.serial} selected={isSelected}>
<TableCell
onClick={(event) => handleClick(event, row.serial)}
padding="checkbox"
>
<Checkbox checked={isSelected} />
</TableCell>
{CardsHeadCells.map((headCell) => (
Expand All @@ -45,11 +101,13 @@ export const CardsTableBody: FC<CardsTableBodyProps> = ({
align={headCell.align}
padding={headCell.padding}
>
<Typography>
<Typography display="inline">
{headCell.convert
? headCell.convert(row[headCell.id])
: (row[headCell.id] as string)}
</Typography>
{headCell.id === "name" &&
editButton(row.id, row[headCell.id])}
</TableCell>
))}
</TableRow>
Expand Down
Loading

0 comments on commit 1864bdc

Please sign in to comment.