Allow users to edit their own order sets
This commit is contained in:
parent
06002c579b
commit
fd654153a4
10 changed files with 273 additions and 161 deletions
35
flask/api.py
35
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/<username>/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/<username>/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/<username>/sets/', methods=['POST'])
|
||||
@api.route('/orders/<username>/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/<username>/sets/<set_id>', methods = ['GET', 'POST', 'DELETE'])
|
||||
@api.route('/orders/<username>/sets/<set_id>', methods = ['GET', 'POST', 'DELETE'])
|
||||
@login_required
|
||||
@authorized_sub
|
||||
def sub_order_set(username, set_id, sub):
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ def index():
|
|||
return redirect('/dashboard/')
|
||||
|
||||
@app.route('/dashboard/', defaults={'path': ''})
|
||||
@app.route('/orders/<path:path>')
|
||||
@app.route('/dashboard/<path:path>')
|
||||
@login_required
|
||||
def dashboard(path):
|
||||
|
|
|
|||
71
flask/vite/src/Dashboard.tsx
Normal file
71
flask/vite/src/Dashboard.tsx
Normal file
|
|
@ -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<SubsListProps> = ({ subs }) => (
|
||||
<>
|
||||
<Title order={1} mb="lg">
|
||||
Subs
|
||||
</Title>
|
||||
<Flex gap="md" wrap="wrap">
|
||||
{subs.map(({ sub_username, telegram_photo_url }) => (
|
||||
<Card
|
||||
key={sub_username}
|
||||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg="gray.2"
|
||||
w="320px"
|
||||
>
|
||||
{telegram_photo_url ? (
|
||||
<Card.Section>
|
||||
<Image
|
||||
src={telegram_photo_url}
|
||||
height={280}
|
||||
alt={`Profile picture for ${sub_username}`}
|
||||
/>
|
||||
</Card.Section>
|
||||
) : null}
|
||||
|
||||
<Flex direction="column" gap="md" mt="md" h="100%">
|
||||
<Text fw={500}>{sub_username}</Text>
|
||||
<Flex gap="md" justify="end">
|
||||
<NavigateButton to={`/orders/${sub_username}`}>
|
||||
<IconPencil style={{ marginRight: "0.5rem" }} />
|
||||
Edit
|
||||
</NavigateButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
|
||||
export const Dashboard: React.FC = () => {
|
||||
const [orderSets, subs] =
|
||||
useLoaderData<[OrderSetProps["orderSets"], SubsListProps["subs"]]>();
|
||||
const { username } = useUserContext();
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<OrderSets orderSets={orderSets} username={username} />
|
||||
{subs.length > 0 ? <SubsList subs={subs} /> : null}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<string>;
|
||||
}) =>
|
||||
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<string>;
|
||||
}) => {
|
||||
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<OrderSet, "orders"> & {
|
|||
orders: FormOrderSetOrder[];
|
||||
};
|
||||
|
||||
export const SubOrderSet: React.FC = () => {
|
||||
export const OrderSet: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { username, set_id } = useParams();
|
||||
const orderSet = useLoaderData<OrderSet>();
|
||||
|
|
@ -80,7 +80,7 @@ export const SubOrderSet: React.FC = () => {
|
|||
orderSet?.scheduled,
|
||||
);
|
||||
|
||||
const form = useForm<FormOrderSet>({
|
||||
const form = useForm<Partial<FormOrderSet>>({
|
||||
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 = () => {
|
|||
<Container pb="xl">
|
||||
<Box mb="lg">
|
||||
<Title order={1}>{orderSet?.name || "New Order Set"}</Title>
|
||||
<Link to={`/dashboard/subs/${username}`}>Return to {username}</Link>
|
||||
<Link to={`/orders/${username}`}>Return to {username}</Link>
|
||||
</Box>
|
||||
<form id="order-set" onSubmit={handleSubmit}>
|
||||
<TextInput {...form.getInputProps("name")} label="Name" />
|
||||
95
flask/vite/src/OrderSets.tsx
Normal file
95
flask/vite/src/OrderSets.tsx
Normal file
|
|
@ -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<OrderSetOrder, "id" | "name"> })[];
|
||||
username: string;
|
||||
linkBack?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const OrderSets: React.FC<OrderSetProps> = ({
|
||||
orderSets,
|
||||
username,
|
||||
linkBack,
|
||||
}) => {
|
||||
const fetcher = useFetcher();
|
||||
const handleDelete = React.useCallback(
|
||||
(id: number) => {
|
||||
fetcher.submit(null, {
|
||||
action: `/orders/${username}/${id}`,
|
||||
method: "DELETE",
|
||||
});
|
||||
},
|
||||
[fetcher],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box mb="lg">
|
||||
<Title order={1}>Order Sets for {username}</Title>
|
||||
{linkBack ? linkBack : null}
|
||||
</Box>
|
||||
<Flex gap="md" wrap="wrap">
|
||||
{orderSets
|
||||
? orderSets.map(
|
||||
({ id, name, scheduled, orders, time, weekdays, weekends }) => (
|
||||
<Card
|
||||
key={id}
|
||||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg="gray.2"
|
||||
w="400px"
|
||||
>
|
||||
<Flex direction="column" gap="md" h="100%">
|
||||
<Title order={4}>{name}</Title>
|
||||
{scheduled ? (
|
||||
<Flex gap="md" align="center">
|
||||
<Text>
|
||||
Scheduled: <TimeValue value={time} format="12h" />
|
||||
</Text>
|
||||
{weekdays ? <Badge color="blue">Weekdays</Badge> : null}
|
||||
{weekends ? <Badge color="blue">Weekends</Badge> : null}
|
||||
</Flex>
|
||||
) : null}
|
||||
<Text mb="md" flex={1}>
|
||||
Orders: {orders.length}
|
||||
</Text>
|
||||
<Flex gap="md" justify="end">
|
||||
<ConfirmDialogButton
|
||||
buttonColor="red.8"
|
||||
buttonText="Delete"
|
||||
text={`Are you sure you want to delete ${name}?`}
|
||||
onConfirm={() => handleDelete(id)}
|
||||
>
|
||||
<IconTrash />
|
||||
</ConfirmDialogButton>
|
||||
<NavigateButton to={`/orders/${username}/${id}`}>
|
||||
<IconPencil style={{ marginRight: "0.5rem" }} />
|
||||
Edit
|
||||
</NavigateButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
),
|
||||
)
|
||||
: null}
|
||||
</Flex>
|
||||
<NavigateButton to={`/orders/${username}/new`}>
|
||||
<IconPlus style={{ marginRight: "0.5rem" }} />
|
||||
New
|
||||
</NavigateButton>
|
||||
<Box mb="lg"></Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<string>;
|
||||
}) => 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<OrderSetOrder, "id" | "name">[];
|
||||
})[]
|
||||
>();
|
||||
|
||||
const handleDelete = React.useCallback(
|
||||
(id: number) => {
|
||||
fetcher.submit(null, {
|
||||
action: `/dashboard/subs/${sub_username}/${id}`,
|
||||
method: "DELETE",
|
||||
});
|
||||
},
|
||||
[fetcher],
|
||||
);
|
||||
const orderSets = useLoaderData<OrderSetProps["orderSets"]>();
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Box mb="lg">
|
||||
<Title order={1}>Order Sets for {sub_username}</Title>
|
||||
<Link to={`/dashboard/`}>Return to all subs</Link>
|
||||
</Box>
|
||||
<Flex gap="md" wrap="wrap">
|
||||
{orderSets
|
||||
? orderSets.map(
|
||||
({ id, name, scheduled, orders, time, weekdays, weekends }) => (
|
||||
<Card
|
||||
key={id}
|
||||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg="gray.2"
|
||||
w="400px"
|
||||
>
|
||||
<Flex direction="column" gap="md" h="100%">
|
||||
<Title order={4}>{name}</Title>
|
||||
{scheduled ? (
|
||||
<Flex gap="md" align="center">
|
||||
<Text>
|
||||
Scheduled: <TimeValue value={time} format="12h" />
|
||||
</Text>
|
||||
{weekdays ? <Badge color="blue">Weekdays</Badge> : null}
|
||||
{weekends ? <Badge color="blue">Weekends</Badge> : null}
|
||||
</Flex>
|
||||
) : null}
|
||||
<Text mb="md" flex={1}>
|
||||
Orders: {orders.length}
|
||||
</Text>
|
||||
<Flex gap="md" justify="end">
|
||||
<ConfirmDialogButton
|
||||
buttonColor="red.8"
|
||||
buttonText="Delete"
|
||||
text={`Are you sure you want to delete ${name}?`}
|
||||
onConfirm={() => handleDelete(id)}
|
||||
>
|
||||
<IconTrash />
|
||||
</ConfirmDialogButton>
|
||||
<NavigateButton
|
||||
to={`/dashboard/subs/${sub_username}/${id}`}
|
||||
>
|
||||
<IconPencil style={{ marginRight: "0.5rem" }} />
|
||||
Edit
|
||||
</NavigateButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
),
|
||||
)
|
||||
: null}
|
||||
</Flex>
|
||||
<NavigateButton to={`/dashboard/subs/${sub_username}/new`}>
|
||||
<IconPlus style={{ marginRight: "0.5rem" }} />
|
||||
New
|
||||
</NavigateButton>
|
||||
<OrderSets
|
||||
username={sub_username}
|
||||
orderSets={orderSets}
|
||||
linkBack={<Link to={`/dashboard/`}>Return to dashboard</Link>}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Container>
|
||||
<Title order={1} mb="lg">
|
||||
Subs
|
||||
</Title>
|
||||
<Flex gap="md" wrap="wrap">
|
||||
{subs.map(({ sub_username, telegram_photo_url }) => (
|
||||
<Card
|
||||
key={sub_username}
|
||||
shadow="sm"
|
||||
padding="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg="gray.2"
|
||||
w="320px"
|
||||
>
|
||||
{telegram_photo_url ? (
|
||||
<Card.Section>
|
||||
<Image
|
||||
src={telegram_photo_url}
|
||||
height={280}
|
||||
alt={`Profile picture for ${sub_username}`}
|
||||
/>
|
||||
</Card.Section>
|
||||
) : null}
|
||||
|
||||
<Flex direction="column" gap="md" mt="md" h="100%">
|
||||
<Text fw={500}>{sub_username}</Text>
|
||||
<Flex gap="md" justify="end">
|
||||
<NavigateButton to={`/dashboard/subs/${sub_username}`}>
|
||||
<IconPencil style={{ marginRight: "0.5rem" }} />
|
||||
Edit
|
||||
</NavigateButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
</Flex>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
35
flask/vite/src/UserContext.tsx
Normal file
35
flask/vite/src/UserContext.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import React from "react";
|
||||
|
||||
export interface UserContextData {
|
||||
username?: string;
|
||||
}
|
||||
|
||||
const UserContext = React.createContext<UserContextData>({});
|
||||
|
||||
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 (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
username,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
@ -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(
|
||||
<React.StrictMode>
|
||||
<MantineProvider theme={theme}>
|
||||
<Notifications />
|
||||
<RouterProvider router={router} />
|
||||
<UserContextProvider>
|
||||
<Notifications />
|
||||
<RouterProvider router={router} />
|
||||
</UserContextProvider>
|
||||
</MantineProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
|
|
|||
8
flask/vite/tsconfig.json
Normal file
8
flask/vite/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["ES2015", "DOM"],
|
||||
"typeRoots": ["src/*.d.ts"],
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue