diff --git a/db/queries.py b/db/queries.py index 8084cc5..9859dd4 100644 --- a/db/queries.py +++ b/db/queries.py @@ -25,6 +25,9 @@ def user_get(username): def orders_pool_list(user_id): return OrdersPool.select().where(OrdersPool.user_id == user_id) +def orders_pool(user_id, set_id): + return OrdersPool.get(OrdersPool.user_id == user_id, OrdersPool.id == set_id) + def domsubusers_add(sub, dom): return DomSubUsers.create( sub=sub, diff --git a/flask/api.py b/flask/api.py index ad8a1bf..fc00969 100644 --- a/flask/api.py +++ b/flask/api.py @@ -1,6 +1,7 @@ -from flask import Blueprint, jsonify, abort +from flask import Blueprint, jsonify, abort, request from flask_login import current_user -from db.queries import user_get, domsubusers_list, orders_pool_list +from db.models import database, Order +from db.queries import user_get, domsubusers_list, orders_pool_list, orders_pool api = Blueprint('api', __name__) @@ -14,19 +15,70 @@ def subs(): ] ) -@api.route('/subs//orders') -def sub_orders(username): +@api.route('/subs//sets') +def sub_order_sets(username): try: sub = user_get(username) except: - abort(500) + abort(403) return if sub.telegram_username not in [dsu.sub.telegram_username for dsu in domsubusers_list(current_user.db_user)]: - abort(500) + abort(403) return return jsonify([ + { + 'id': op.id, + 'name': op.name, + } + for op + in orders_pool_list(sub.id) + ]) + +@api.route('/subs//sets/', methods = ['GET', 'POST']) +def sub_order_set(username, set_id): + try: + sub = user_get(username) + except: + abort(403) + return + + if sub.telegram_username not in [dsu.sub.telegram_username for dsu in domsubusers_list(current_user.db_user)]: + abort(403) + return + + op = orders_pool(sub.id, set_id) + + if request.method == 'POST': + with database.atomic() as transaction: + try: + op.name = request.json['name'] + op.save() + + for updated_order in request.json['orders']: + if isinstance(updated_order['id'], int): + order_to_update = op.orders.where(Order.id == updated_order['id']).get() + + if '_delete' in updated_order: + order_to_update.delete_instance(recursive=True) + else: + order_to_update.name = updated_order['name'] + order_to_update.weight = updated_order['weight'] + order_to_update.repeat = updated_order['repeat'] + + order_to_update.save() + else: + Order.create( + pool=op, + name=updated_order['name'], + weight=updated_order['weight'], + repeat=updated_order['repeat'] + ) + except: + transaction.rollback() + + return jsonify( { 'id': op.id, 'name': op.name, @@ -34,9 +86,20 @@ def sub_orders(username): 'id': order.id, 'name': order.name, 'weight': order.weight, - 'repeat': order.repeat + 'repeat': order.repeat, + 'add_ons': [ + { + 'id': 1, + 'name': 'Leg Shackles', + 'probability': 0.9 + }, + { + 'id': 2, + 'name': 'Leather Collar', + 'probability': 0.5 + } + ] } for order in op.orders] } - for op - in orders_pool_list(sub.id) - ]) + ) + diff --git a/flask/vite/package.json b/flask/vite/package.json index 6b5a83c..e682650 100644 --- a/flask/vite/package.json +++ b/flask/vite/package.json @@ -15,6 +15,8 @@ }, "dependencies": { "@mantine/core": "^8.3.12", + "@mantine/form": "^8.3.13", + "@tabler/icons-react": "^3.36.1", "postcss": "^8.5.6", "react": "^19.2.3", "react-dom": "^19.2.3", diff --git a/flask/vite/src/SubOrderSet.tsx b/flask/vite/src/SubOrderSet.tsx new file mode 100644 index 0000000..6fa1a86 --- /dev/null +++ b/flask/vite/src/SubOrderSet.tsx @@ -0,0 +1,226 @@ +import { + Container, + Paper, + Input, + Slider, + TextInput, + Button, + Divider, + Flex, + ActionIcon, +} from "@mantine/core"; +import { randomId } from "@mantine/hooks"; +import { useForm } from "@mantine/form"; +import React from "react"; +import { Params, useLoaderData, useParams, useNavigate } from "react-router"; +import { IconMinus, IconPlus } from "@tabler/icons-react"; + +export const subOrderSetLoader = async ({ + params: { username, set_id }, +}: { + params: Params; +}) => + fetch(`/api/subs/${username}/sets/${set_id}`).then((response) => + response.json() + ); + +type FormOrderSetOrderAddOn = Omit & { + id: number | string; + _delete?: boolean; +}; + +type FormOrderSetOrder = Omit & { + id: number | string; + _delete?: boolean; + add_ons: FormOrderSetOrderAddOn[]; +}; + +type FormOrderSet = Omit & { + orders: FormOrderSetOrder[]; +}; + +export const SubOrderSet: React.FC = () => { + const navigate = useNavigate(); + const { username, set_id } = useParams(); + const { id, name, orders } = useLoaderData(); + + const form = useForm({ + mode: "uncontrolled", + initialValues: { id, name, orders }, + validate: { + name: (value) => (value.length < 1 ? "Please enter a name" : null), + orders: { + name: (value) => (value.length < 1 ? "Please enter a name" : null), + add_ons: { + name: (value) => (value.length < 1 ? "Please enter a name" : null), + }, + }, + }, + }); + + const handleSubmit = React.useCallback( + form.onSubmit((values) => { + fetch(`/api/subs/${username}/sets/${set_id}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(values), + }).then((response) => { + if (response.ok) { + navigate(`/dashboard/subs/${username}`); + } else { + console.error(response.statusText); + } + }); + }), + [form] + ); + + const handleNewOrder = React.useCallback(() => { + form.insertListItem("orders", { + id: `new_${randomId()}`, + name: "", + weight: 10, + repeat: 0.8, + add_ons: [], + }); + }, [form]); + + const handleRemoveOrder = React.useCallback( + (idx: number) => { + const orderId = form.getValues().orders[idx].id.toString(); + + if (orderId.indexOf("new_") === 0) { + form.removeListItem("orders", idx); + } else { + form.setFieldValue(`orders.${idx}._delete`, true); + } + }, + [form] + ); + + const handleNewAddOn = React.useCallback( + (idx: number) => { + form.insertListItem(`orders.${idx}.add_ons`, { + id: `new_${randomId()}`, + name: "", + probability: 0.5, + }); + }, + [form] + ); + + const handleRemoveAddOn = React.useCallback( + (idx: number, add_on_idx: number) => { + const addOnId = form + .getValues() + .orders[idx].add_ons[add_on_idx].id.toString(); + + if (addOnId.indexOf("new_") === 0) { + form.removeListItem(`orders.${idx}.add_ons`, add_on_idx); + } else { + form.setFieldValue(`orders.${idx}.add_ons.${add_on_idx}._delete`, true); + } + }, + [form] + ); + + return ( +
+ +

{name}

+ <> + + {form.getValues().orders.map(({ id: order_id, _delete }, idx) => + _delete ? null : ( + + + + handleRemoveOrder(idx)} + > + + + + + + + + + +

Add-On

+ {form + .getValues() + .orders[idx].add_ons.map( + ({ id: add_on_id, _delete: _add_on_delete }, add_on_idx) => + _add_on_delete ? null : ( + + + + handleRemoveAddOn(idx, add_on_idx)} + > + + + + + + + + ) + )} + +
+ ) + )} + + + + + + +
+
+ ); +}; diff --git a/flask/vite/src/SubOrderSets.tsx b/flask/vite/src/SubOrderSets.tsx new file mode 100644 index 0000000..09720bb --- /dev/null +++ b/flask/vite/src/SubOrderSets.tsx @@ -0,0 +1,42 @@ +import { + Container, + Accordion, + TextInput, + Divider, + Input, + Slider, +} from "@mantine/core"; +import React from "react"; +import { Link, Params, useLoaderData, useParams } from "react-router"; + +export const subOrderSetsLoader = async ({ + params: { username }, +}: { + params: Params; +}) => fetch(`/api/subs/${username}/sets`).then((response) => response.json()); + +export const SubOrderSets: React.FC = () => { + const { username: sub_username } = useParams(); + const orders = useLoaderData< + { + id: number; + name: string; + orders: Pick[]; + }[] + >(); + + return ( + +

Orders for {sub_username}

+ + {orders + ? orders.map(({ id, name }) => ( + + {name} + + )) + : null} + +
+ ); +}; diff --git a/flask/vite/src/SubOrders.tsx b/flask/vite/src/SubOrders.tsx deleted file mode 100644 index 40edd44..0000000 --- a/flask/vite/src/SubOrders.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { - Container, - Accordion, - TextInput, - Divider, - Input, - Slider, -} from "@mantine/core"; -import React from "react"; -import { Params, useLoaderData, useParams } from "react-router"; - -export const subOrdersLoader = async ({ - params: { username }, -}: { - params: Params; -}) => fetch(`/api/subs/${username}/orders`).then((response) => response.json()); - -export const SubOrders: React.FC = () => { - const { username: sub_username } = useParams(); - const orders = useLoaderData< - { - id: number; - name: string; - orders: { - id: number; - name: string; - weight: number; - repeat: number; - }[]; - }[] - >(); - - return ( - -

Orders for {sub_username}

- - {orders - ? orders.map(({ id, name, orders }) => ( - - {name} - - <> - - {orders.map( - ({ id: order_id, name: order_name, weight, repeat }) => ( - - - - - - - - - - - ) - )} - - - - )) - : null} - -
- ); -}; diff --git a/flask/vite/src/index.d.ts b/flask/vite/src/index.d.ts new file mode 100644 index 0000000..2405b77 --- /dev/null +++ b/flask/vite/src/index.d.ts @@ -0,0 +1,19 @@ +type OrderSetOrderAddOn = { + id: number; + name: string; + probability: number; +} + +type OrderSetOrder = { + id: number; + name: string; + weight: number; + repeat: number; + add_ons: OrderSetOrderAddOn[]; +} + +type OrderSet = { + id: number; + name: string; + orders: OrderSetOrder[]; +} diff --git a/flask/vite/src/main.tsx b/flask/vite/src/main.tsx index 29b0f81..3ddce94 100644 --- a/flask/vite/src/main.tsx +++ b/flask/vite/src/main.tsx @@ -2,21 +2,43 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { createBrowserRouter } from "react-router"; import { RouterProvider } from "react-router/dom"; -import { createTheme, MantineProvider, Input } from "@mantine/core"; +import { + createTheme, + MantineProvider, + Input, + Paper, + Slider, +} from "@mantine/core"; import "@mantine/core/styles.css"; import { SubsList, subsListLoader } from "./SubsList"; -import { SubOrders, subOrdersLoader } from "./SubOrders"; +import { SubOrderSets, subOrderSetsLoader } from "./SubOrderSets"; +import { SubOrderSet, subOrderSetLoader } from "./SubOrderSet"; const theme = createTheme({ components: { InputWrapper: Input.Wrapper.extend({ defaultProps: { inputWrapperOrder: ["label", "input", "description", "error"], - my: "md", + mb: "xs", }, }), + Paper: Paper.extend({ + defaultProps: { + p: "sm", + shadow: "xs", + mb: "lg", + }, + }), + Slider: Slider.extend({ + vars: (theme) => + ({ + root: { + "--slider-track-bg": theme.colors.gray[4], + }, + } as any), + }), }, }); @@ -27,9 +49,14 @@ const router = createBrowserRouter([ loader: subsListLoader, }, { - path: "dashboard/subs/:username", - Component: SubOrders, - loader: subOrdersLoader, + path: "dashboard/subs/:username/", + Component: SubOrderSets, + loader: subOrderSetsLoader, + }, + { + path: "dashboard/subs/:username/:set_id", + Component: SubOrderSet, + loader: subOrderSetLoader, }, ]);