Compare commits
3 Commits
4aba3db888
...
1b9d1ee7d5
Author | SHA1 | Date | |
---|---|---|---|
1b9d1ee7d5 | |||
1f7066a2d9 | |||
36fadc059a |
73
frontend/package-lock.json
generated
73
frontend/package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"@mui/lab": "^5.0.0-alpha.91",
|
"@mui/lab": "^5.0.0-alpha.91",
|
||||||
"@mui/material": "^5.9.1",
|
"@mui/material": "^5.9.1",
|
||||||
"@mui/x-date-pickers": "^5.0.0-beta.2",
|
"@mui/x-date-pickers": "^5.0.0-beta.2",
|
||||||
|
"@tanstack/react-query": "^4.33.0",
|
||||||
"@testing-library/jest-dom": "^5.16.4",
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
"@testing-library/react": "^13.3.0",
|
"@testing-library/react": "^13.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
@ -22,6 +23,7 @@
|
|||||||
"@types/node": "^16.11.45",
|
"@types/node": "^16.11.45",
|
||||||
"@types/react": "^18.0.15",
|
"@types/react": "^18.0.15",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"axios": "^1.5.0",
|
||||||
"date-fns": "^2.29.1",
|
"date-fns": "^2.29.1",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@ -3953,6 +3955,41 @@
|
|||||||
"url": "https://github.com/sponsors/gregberge"
|
"url": "https://github.com/sponsors/gregberge"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "4.33.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.33.0.tgz",
|
||||||
|
"integrity": "sha512-qYu73ptvnzRh6se2nyBIDHGBQvPY1XXl3yR769B7B6mIDD7s+EZhdlWHQ67JI6UOTFRaI7wupnTnwJ3gE0Mr/g==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "4.33.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.33.0.tgz",
|
||||||
|
"integrity": "sha512-97nGbmDK0/m0B86BdiXzx3EW9RcDYKpnyL2+WwyuLHEgpfThYAnXFaMMmnTDuAO4bQJXEhflumIEUfKmP7ESGA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "4.33.0",
|
||||||
|
"use-sync-external-store": "^1.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-native": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@testing-library/dom": {
|
"node_modules/@testing-library/dom": {
|
||||||
"version": "9.3.1",
|
"version": "9.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz",
|
||||||
@ -5379,6 +5416,29 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axios/node_modules/form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
|
||||||
@ -13438,6 +13498,11 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"node_modules/psl": {
|
"node_modules/psl": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||||
@ -15914,6 +15979,14 @@
|
|||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"@mui/lab": "^5.0.0-alpha.91",
|
"@mui/lab": "^5.0.0-alpha.91",
|
||||||
"@mui/material": "^5.9.1",
|
"@mui/material": "^5.9.1",
|
||||||
"@mui/x-date-pickers": "^5.0.0-beta.2",
|
"@mui/x-date-pickers": "^5.0.0-beta.2",
|
||||||
|
"@tanstack/react-query": "^4.33.0",
|
||||||
"@testing-library/jest-dom": "^5.16.4",
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
"@testing-library/react": "^13.3.0",
|
"@testing-library/react": "^13.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
@ -17,6 +18,7 @@
|
|||||||
"@types/node": "^16.11.45",
|
"@types/node": "^16.11.45",
|
||||||
"@types/react": "^18.0.15",
|
"@types/react": "^18.0.15",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"axios": "^1.5.0",
|
||||||
"date-fns": "^2.29.1",
|
"date-fns": "^2.29.1",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
// Roboto font and its weights
|
// Roboto font and its weights
|
||||||
import "@fontsource/roboto/300.css";
|
import "@fontsource/roboto/300.css";
|
||||||
import "@fontsource/roboto/400.css";
|
import "@fontsource/roboto/400.css";
|
||||||
@ -22,21 +24,32 @@ import { AuthContextProvider } from "./AuthContext";
|
|||||||
// React router
|
// React router
|
||||||
import Router from "./Router";
|
import Router from "./Router";
|
||||||
|
|
||||||
function App() {
|
// Toasts
|
||||||
|
import Toasts from "./components/Toasts";
|
||||||
|
import { ToastContextProvider } from "./ToastContext";
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthContextProvider>
|
<AuthContextProvider>
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Todo</title>
|
<title>Todo</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
|
<ToastContextProvider>
|
||||||
<Container maxWidth="md">
|
<Container maxWidth="md">
|
||||||
|
<Toasts />
|
||||||
<Router />
|
<Router />
|
||||||
</Container>
|
</Container>
|
||||||
|
</ToastContextProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
</AuthContextProvider>
|
</AuthContextProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
49
frontend/src/ToastContext.tsx
Normal file
49
frontend/src/ToastContext.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { AlertColor } from "@mui/material/Alert";
|
||||||
|
|
||||||
|
import React, { createContext, useState } from "react";
|
||||||
|
|
||||||
|
export interface IToast {
|
||||||
|
category?: AlertColor;
|
||||||
|
key: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IToastContext {
|
||||||
|
addToast: (message: string, category: AlertColor | undefined) => void;
|
||||||
|
setToasts: React.Dispatch<React.SetStateAction<IToast[]>>;
|
||||||
|
toasts: IToast[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToastContext = createContext<IToastContext>({
|
||||||
|
addToast: () => {},
|
||||||
|
setToasts: () => {},
|
||||||
|
toasts: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToastContextProvider = ({ children }: IProps) => {
|
||||||
|
const [toasts, setToasts] = useState<IToast[]>([]);
|
||||||
|
|
||||||
|
const addToast = (
|
||||||
|
message: string,
|
||||||
|
category: AlertColor | undefined = undefined,
|
||||||
|
) => {
|
||||||
|
setToasts((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
category,
|
||||||
|
key: new Date().getTime(),
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={{ addToast, setToasts, toasts }}>
|
||||||
|
{children}
|
||||||
|
</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -8,7 +8,7 @@ interface IProps {
|
|||||||
const Title = ({ title }: IProps) => (
|
const Title = ({ title }: IProps) => (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Tozo | {title}</title>
|
<title>Todo | {title}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Typography component="h1" variant="h5">
|
<Typography component="h1" variant="h5">
|
||||||
{title}
|
{title}
|
||||||
|
47
frontend/src/components/Toasts.tsx
Normal file
47
frontend/src/components/Toasts.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import Alert from "@mui/material/Alert";
|
||||||
|
import Snackbar from "@mui/material/Snackbar";
|
||||||
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { ToastContext, IToast } from "src/ToastContext";
|
||||||
|
|
||||||
|
const Toasts = () => {
|
||||||
|
const { toasts, setToasts } = useContext(ToastContext);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [currentToast, setCurrentToast] = useState<IToast | undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open && toasts.length) {
|
||||||
|
setCurrentToast(toasts[0]);
|
||||||
|
setToasts((prev) => prev.slice(1));
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}, [open, setCurrentToast, setOpen, setToasts, toasts]);
|
||||||
|
|
||||||
|
const onClose = (event?: React.SyntheticEvent | Event, reason?: string) => {
|
||||||
|
if (reason !== "clickaway") {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Snackbar
|
||||||
|
anchorOrigin={{
|
||||||
|
horizontal: "center",
|
||||||
|
vertical: "top",
|
||||||
|
}}
|
||||||
|
autoHideDuration={6000}
|
||||||
|
key={currentToast?.key}
|
||||||
|
onClose={onClose}
|
||||||
|
open={open}
|
||||||
|
TransitionProps={{
|
||||||
|
onExited: () => setCurrentToast(undefined),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Alert onClose={onClose} severity={currentToast?.category}>
|
||||||
|
{currentToast?.message}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Toasts;
|
36
frontend/src/models.tsx
Normal file
36
frontend/src/models.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { formatISO } from "date-fns";
|
||||||
|
import * as yup from "yup";
|
||||||
|
|
||||||
|
const todoSchema = yup.object({
|
||||||
|
complete: yup.boolean().required(),
|
||||||
|
due: yup.date().nullable(),
|
||||||
|
id: yup.number().required().positive().integer(),
|
||||||
|
task: yup.string().trim().min(1).defined().strict(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
export class Todo {
|
||||||
|
complete: boolean;
|
||||||
|
due: Date | null;
|
||||||
|
id: number;
|
||||||
|
task: string;
|
||||||
|
|
||||||
|
constructor(data: any) {
|
||||||
|
const validatedData = todoSchema.validateSync(data);
|
||||||
|
this.complete = validatedData.complete;
|
||||||
|
this.due = validatedData.due ?? null;
|
||||||
|
this.id = validatedData.id;
|
||||||
|
this.task = validatedData.task;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): any {
|
||||||
|
return {
|
||||||
|
complete: this.complete,
|
||||||
|
due:
|
||||||
|
this.due !== null
|
||||||
|
? formatISO(this.due, { representation: "date" })
|
||||||
|
: null,
|
||||||
|
id: this.id,
|
||||||
|
task: this.task,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
80
frontend/src/query.ts
Normal file
80
frontend/src/query.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import axios, { AxiosError } from "axios";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import {
|
||||||
|
MutationFunction,
|
||||||
|
QueryFunction,
|
||||||
|
QueryFunctionContext,
|
||||||
|
QueryKey,
|
||||||
|
useMutation as useReactMutation,
|
||||||
|
UseMutationOptions,
|
||||||
|
UseMutationResult,
|
||||||
|
useQuery as useReactQuery,
|
||||||
|
UseQueryOptions,
|
||||||
|
UseQueryResult,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { AuthContext } from "src/AuthContext";
|
||||||
|
|
||||||
|
const MAX_FAILURES = 2;
|
||||||
|
|
||||||
|
export function useQuery<
|
||||||
|
TQueryFnData = unknown,
|
||||||
|
TData = TQueryFnData,
|
||||||
|
TQueryKey extends QueryKey = QueryKey,
|
||||||
|
>(
|
||||||
|
queryKey: TQueryKey,
|
||||||
|
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
||||||
|
options?: UseQueryOptions<TQueryFnData, AxiosError, TData, TQueryKey>,
|
||||||
|
): UseQueryResult<TData, AxiosError> {
|
||||||
|
const { setAuthenticated } = useContext(AuthContext);
|
||||||
|
|
||||||
|
return useReactQuery<TQueryFnData, AxiosError, TData, TQueryKey>(
|
||||||
|
queryKey,
|
||||||
|
async (context: QueryFunctionContext<TQueryKey>) => {
|
||||||
|
try {
|
||||||
|
return await queryFn(context);
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||||
|
setAuthenticated(false);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retry: (failureCount: number, error: AxiosError) =>
|
||||||
|
failureCount < MAX_FAILURES &&
|
||||||
|
(!error.response || error.response.status >= 500),
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMutation<
|
||||||
|
TData = unknown,
|
||||||
|
TVariables = void,
|
||||||
|
TContext = unknown,
|
||||||
|
>(
|
||||||
|
mutationFn: MutationFunction<TData, TVariables>,
|
||||||
|
options?: UseMutationOptions<TData, AxiosError, TVariables, TContext>,
|
||||||
|
): UseMutationResult<TData, AxiosError, TVariables, TContext> {
|
||||||
|
const { setAuthenticated } = useContext(AuthContext);
|
||||||
|
|
||||||
|
return useReactMutation<TData, AxiosError, TVariables, TContext>(
|
||||||
|
async (variables: TVariables) => {
|
||||||
|
try {
|
||||||
|
return await mutationFn(variables);
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||||
|
setAuthenticated(false);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retry: (failureCount: number, error: AxiosError) =>
|
||||||
|
failureCount < MAX_FAILURES &&
|
||||||
|
(!error.response || error.response.status >= 500),
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user