From fd654153a40989435cbd20b59e00d3161015f7f9 Mon Sep 17 00:00:00 2001 From: Johnny Gear Date: Wed, 4 Mar 2026 15:14:18 -0600 Subject: [PATCH] Allow users to edit their own order sets --- flask/api.py | 35 ++++++- flask/app.py | 1 + flask/vite/src/Dashboard.tsx | 71 ++++++++++++++ .../src/{SubOrderSet.tsx => OrderSet.tsx} | 18 ++-- flask/vite/src/OrderSets.tsx | 95 +++++++++++++++++++ flask/vite/src/SubOrderSets.tsx | 86 ++--------------- flask/vite/src/SubsList.tsx | 54 ----------- flask/vite/src/UserContext.tsx | 35 +++++++ flask/vite/src/main.tsx | 31 +++--- flask/vite/tsconfig.json | 8 ++ 10 files changed, 273 insertions(+), 161 deletions(-) create mode 100644 flask/vite/src/Dashboard.tsx rename flask/vite/src/{SubOrderSet.tsx => OrderSet.tsx} (95%) create mode 100644 flask/vite/src/OrderSets.tsx delete mode 100644 flask/vite/src/SubsList.tsx create mode 100644 flask/vite/src/UserContext.tsx create mode 100644 flask/vite/tsconfig.json diff --git a/flask/api.py b/flask/api.py index 4e8d400..d6cd334 100644 --- a/flask/api.py +++ b/flask/api.py @@ -6,6 +6,13 @@ from db.queries import user_get, domsubusers_list, orders_pool_list, orders_pool api = Blueprint('api', __name__) +@api.route("/me") +@login_required +def me(): + return jsonify({ + "username": current_user.db_user.telegram_username + }) + @api.route('/subs') @login_required def subs(): @@ -29,7 +36,8 @@ def authorized_sub(func): abort(403) return - if sub.telegram_username not in [dsu.sub.telegram_username for dsu in domsubusers_list(current_user.db_user)]: + if (sub.id != current_user.db_user.id and + sub.telegram_username not in [dsu.sub.telegram_username for dsu in domsubusers_list(current_user.db_user)]): abort(403) return @@ -37,7 +45,26 @@ def authorized_sub(func): return func(*args, **kwargs) return wrapper -@api.route('/subs//sets') +@api.route('/orders/') +@login_required +def my_order_sets(): + return jsonify([ + { + 'id': op.id, + 'name': op.name, + 'scheduled': op.scheduled, + 'time': op.time, + 'weekends': op.weekends, + 'weekdays': op.weekdays, + 'orders': [{ + 'id': order.id, + } for order in op.orders] + } + for op + in orders_pool_list(current_user.db_user) + ]) + +@api.route('/orders//sets') @login_required @authorized_sub def sub_order_sets(username, sub): @@ -57,7 +84,7 @@ def sub_order_sets(username, sub): in orders_pool_list(sub.id) ]) -@api.route('/subs//sets/', methods=['POST']) +@api.route('/orders//sets/', methods=['POST']) @login_required @authorized_sub def sub_order_set_create(username, sub): @@ -95,7 +122,7 @@ def sub_order_set_create(username, sub): return jsonify(new_order_pool.to_dict()) -@api.route('/subs//sets/', methods = ['GET', 'POST', 'DELETE']) +@api.route('/orders//sets/', methods = ['GET', 'POST', 'DELETE']) @login_required @authorized_sub def sub_order_set(username, set_id, sub): diff --git a/flask/app.py b/flask/app.py index 623dcdd..803f884 100644 --- a/flask/app.py +++ b/flask/app.py @@ -49,6 +49,7 @@ def index(): return redirect('/dashboard/') @app.route('/dashboard/', defaults={'path': ''}) +@app.route('/orders/') @app.route('/dashboard/') @login_required def dashboard(path): diff --git a/flask/vite/src/Dashboard.tsx b/flask/vite/src/Dashboard.tsx new file mode 100644 index 0000000..052c283 --- /dev/null +++ b/flask/vite/src/Dashboard.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { Container, Text, Title, Flex, Card, Image, Box } from "@mantine/core"; +import { useLoaderData } from "react-router"; +import { IconPencil } from "@tabler/icons-react"; +import { NavigateButton } from "./NavigateButton"; +import { OrderSetProps, OrderSets } from "./OrderSets"; +import { useUserContext } from "./UserContext"; + +export const subsListLoader = () => + Promise.all([ + fetch("/api/orders/").then((response) => response.json()), + fetch("/api/subs").then((response) => response.json()), + ]); + +interface SubsListProps { + subs: { sub_username: string; telegram_photo_url: string }[]; +} + +const SubsList: React.FC = ({ subs }) => ( + <> + + Subs + + + {subs.map(({ sub_username, telegram_photo_url }) => ( + + {telegram_photo_url ? ( + + {`Profile + + ) : null} + + + {sub_username} + + + + Edit + + + + + ))} + + +); + +export const Dashboard: React.FC = () => { + const [orderSets, subs] = + useLoaderData<[OrderSetProps["orderSets"], SubsListProps["subs"]]>(); + const { username } = useUserContext(); + + return ( + + + {subs.length > 0 ? : null} + + ); +}; diff --git a/flask/vite/src/SubOrderSet.tsx b/flask/vite/src/OrderSet.tsx similarity index 95% rename from flask/vite/src/SubOrderSet.tsx rename to flask/vite/src/OrderSet.tsx index f0d82f5..bd4d3e6 100644 --- a/flask/vite/src/SubOrderSet.tsx +++ b/flask/vite/src/OrderSet.tsx @@ -31,16 +31,16 @@ import { import { IconMinus, IconPlus } from "@tabler/icons-react"; import { fetchHeaders } from "./fetch"; -export const subOrderSetLoader = async ({ +export const orderSetLoader = async ({ params: { username, set_id }, }: { params: Params; }) => - fetch(`/api/subs/${username}/sets/${set_id}`).then((response) => + fetch(`/api/orders/${username}/sets/${set_id}`).then((response) => response.json(), ); -export const subOrderSetAction = async ({ +export const orderSetAction = async ({ request, params: { username, set_id: id }, }: { @@ -48,7 +48,7 @@ export const subOrderSetAction = async ({ params: Params; }) => { if (request.method == "DELETE") { - const response = await fetch(`/api/subs/${username}/sets/${id}`, { + const response = await fetch(`/api/orders/${username}/sets/${id}`, { method: "DELETE", headers: fetchHeaders(), }); @@ -72,7 +72,7 @@ type FormOrderSet = Omit & { orders: FormOrderSetOrder[]; }; -export const SubOrderSet: React.FC = () => { +export const OrderSet: React.FC = () => { const navigate = useNavigate(); const { username, set_id } = useParams(); const orderSet = useLoaderData(); @@ -80,7 +80,7 @@ export const SubOrderSet: React.FC = () => { orderSet?.scheduled, ); - const form = useForm({ + const form = useForm>({ mode: "uncontrolled", initialValues: orderSet ?? { scheduled: false, @@ -115,7 +115,7 @@ export const SubOrderSet: React.FC = () => { const handleSubmit = React.useCallback( form.onSubmit((values) => { - fetch(`/api/subs/${username}/sets/${set_id || ""}`, { + fetch(`/api/orders/${username}/sets/${set_id || ""}`, { method: "POST", headers: fetchHeaders(), body: JSON.stringify(values), @@ -126,7 +126,7 @@ export const SubOrderSet: React.FC = () => { message: `Updates to ${values.name} were successful`, color: "green", }); - navigate(`/dashboard/subs/${username}`); + navigate(`/orders/${username}`); } else { notifications.show({ title: "Error", @@ -193,7 +193,7 @@ export const SubOrderSet: React.FC = () => { {orderSet?.name || "New Order Set"} - Return to {username} + Return to {username}
diff --git a/flask/vite/src/OrderSets.tsx b/flask/vite/src/OrderSets.tsx new file mode 100644 index 0000000..2a8cdca --- /dev/null +++ b/flask/vite/src/OrderSets.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import { Title, Card, Text, Flex, Badge, Box } from "@mantine/core"; +import { TimeValue } from "@mantine/dates"; +import { IconPencil, IconPlus, IconTrash } from "@tabler/icons-react"; + +import { ConfirmDialogButton } from "./ConfirmDialogButton"; +import { NavigateButton } from "./NavigateButton"; +import { useFetcher } from "react-router"; + +export interface OrderSetProps { + orderSets: (Pick< + OrderSet, + "id" | "name" | "scheduled" | "time" | "weekends" | "weekdays" | "orders" + > & { orders: Pick })[]; + username: string; + linkBack?: React.ReactNode; +} + +export const OrderSets: React.FC = ({ + orderSets, + username, + linkBack, +}) => { + const fetcher = useFetcher(); + const handleDelete = React.useCallback( + (id: number) => { + fetcher.submit(null, { + action: `/orders/${username}/${id}`, + method: "DELETE", + }); + }, + [fetcher], + ); + + return ( + <> + + Order Sets for {username} + {linkBack ? linkBack : null} + + + {orderSets + ? orderSets.map( + ({ id, name, scheduled, orders, time, weekdays, weekends }) => ( + + + {name} + {scheduled ? ( + + + Scheduled: + + {weekdays ? Weekdays : null} + {weekends ? Weekends : null} + + ) : null} + + Orders: {orders.length} + + + handleDelete(id)} + > + + + + + Edit + + + + + ), + ) + : null} + + + + New + + + + ); +}; diff --git a/flask/vite/src/SubOrderSets.tsx b/flask/vite/src/SubOrderSets.tsx index 3b8dbae..97dcc69 100644 --- a/flask/vite/src/SubOrderSets.tsx +++ b/flask/vite/src/SubOrderSets.tsx @@ -11,95 +11,25 @@ import { } from "react-router"; import { ConfirmDialogButton } from "./ConfirmDialogButton"; import { NavigateButton } from "./NavigateButton"; -import { fetchHeaders } from "./fetch"; +import { OrderSetProps, OrderSets } from "./OrderSets"; export const subOrderSetsLoader = async ({ params: { username }, }: { params: Params; -}) => fetch(`/api/subs/${username}/sets`).then((response) => response.json()); +}) => fetch(`/api/orders/${username}/sets`).then((response) => response.json()); export const SubOrderSets: React.FC = () => { - const fetcher = useFetcher(); const { username: sub_username } = useParams(); - const orderSets = useLoaderData< - (Pick< - OrderSet, - "id" | "name" | "scheduled" | "time" | "weekends" | "weekdays" | "orders" - > & { - orders: Pick[]; - })[] - >(); - - const handleDelete = React.useCallback( - (id: number) => { - fetcher.submit(null, { - action: `/dashboard/subs/${sub_username}/${id}`, - method: "DELETE", - }); - }, - [fetcher], - ); + const orderSets = useLoaderData(); return ( - - Order Sets for {sub_username} - Return to all subs - - - {orderSets - ? orderSets.map( - ({ id, name, scheduled, orders, time, weekdays, weekends }) => ( - - - {name} - {scheduled ? ( - - - Scheduled: - - {weekdays ? Weekdays : null} - {weekends ? Weekends : null} - - ) : null} - - Orders: {orders.length} - - - handleDelete(id)} - > - - - - - Edit - - - - - ), - ) - : null} - - - - New - + Return to dashboard} + /> ); }; diff --git a/flask/vite/src/SubsList.tsx b/flask/vite/src/SubsList.tsx deleted file mode 100644 index 81511d6..0000000 --- a/flask/vite/src/SubsList.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from "react"; -import { Container, Text, Title, Flex, Card, Image } from "@mantine/core"; -import { useLoaderData } from "react-router"; -import { IconPencil } from "@tabler/icons-react"; -import { NavigateButton } from "./NavigateButton"; - -export const subsListLoader = () => - fetch("/api/subs").then((response) => response.json()); - -export const SubsList: React.FC = () => { - const subs = - useLoaderData<{ sub_username: string; telegram_photo_url: string }[]>(); - - return ( - - - Subs - - - {subs.map(({ sub_username, telegram_photo_url }) => ( - - {telegram_photo_url ? ( - - {`Profile - - ) : null} - - - {sub_username} - - - - Edit - - - - - ))} - - - ); -}; diff --git a/flask/vite/src/UserContext.tsx b/flask/vite/src/UserContext.tsx new file mode 100644 index 0000000..0eceb0d --- /dev/null +++ b/flask/vite/src/UserContext.tsx @@ -0,0 +1,35 @@ +import React from "react"; + +export interface UserContextData { + username?: string; +} + +const UserContext = React.createContext({}); + +export const useUserContext = (): UserContextData => { + return React.useContext(UserContext); +}; + +export const UserContextProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [username, setUsername] = React.useState(); + + React.useEffect(() => { + fetch(`/api/me`) + .then((response) => response.json()) + .then(({ username }) => { + setUsername(username); + }); + }, []); + + return ( + + {children} + + ); +}; diff --git a/flask/vite/src/main.tsx b/flask/vite/src/main.tsx index 49e8e4c..60b2fa8 100644 --- a/flask/vite/src/main.tsx +++ b/flask/vite/src/main.tsx @@ -15,13 +15,10 @@ import "@mantine/core/styles.css"; import "@mantine/dates/styles.css"; import "@mantine/notifications/styles.css"; -import { SubsList, subsListLoader } from "./SubsList"; +import { Dashboard, subsListLoader } from "./Dashboard"; import { SubOrderSets, subOrderSetsLoader } from "./SubOrderSets"; -import { - SubOrderSet, - subOrderSetLoader, - subOrderSetAction, -} from "./SubOrderSet"; +import { OrderSet, orderSetLoader, orderSetAction } from "./OrderSet"; +import { UserContextProvider } from "./UserContext"; const theme = createTheme({ components: { @@ -52,31 +49,33 @@ const theme = createTheme({ const router = createBrowserRouter([ { path: "dashboard", - Component: SubsList, + Component: Dashboard, loader: subsListLoader, }, { - path: "dashboard/subs/:username/", + path: "orders/:username", Component: SubOrderSets, loader: subOrderSetsLoader, }, { - path: "dashboard/subs/:username/new", - Component: SubOrderSet, + path: "orders/:username/new", + Component: OrderSet, }, { - path: "dashboard/subs/:username/:set_id", - Component: SubOrderSet, - loader: subOrderSetLoader, - action: subOrderSetAction, + path: "orders/:username/:set_id", + Component: OrderSet, + loader: orderSetLoader, + action: orderSetAction, }, ]); ReactDOM.createRoot(document.getElementById("root")!).render( - - + + + + , ); diff --git a/flask/vite/tsconfig.json b/flask/vite/tsconfig.json new file mode 100644 index 0000000..b7c9429 --- /dev/null +++ b/flask/vite/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "lib": ["ES2015", "DOM"], + "typeRoots": ["src/*.d.ts"], + "esModuleInterop": true + } +} \ No newline at end of file