Orders Pools - Improve UI

This commit is contained in:
Johnny Gear 2026-03-07 17:54:41 -06:00
parent 257461307e
commit b194b19a15
5 changed files with 153 additions and 92 deletions

View file

@ -140,6 +140,8 @@ def my_order_sets():
'punishment_pool_name': op.punishment_pool.name if op.punishment_pool is not None else None, 'punishment_pool_name': op.punishment_pool.name if op.punishment_pool is not None else None,
'orders': [{ 'orders': [{
'id': order.id, 'id': order.id,
'name': order.name,
'weight': order.weight
} for order in op.orders] } for order in op.orders]
} }
for op for op
@ -162,6 +164,8 @@ def sub_order_sets(username, sub):
'punishment_pool_name': op.punishment_pool.name if op.punishment_pool is not None else None, 'punishment_pool_name': op.punishment_pool.name if op.punishment_pool is not None else None,
'orders': [{ 'orders': [{
'id': order.id, 'id': order.id,
'name': order.name,
'weight': order.weight
} for order in op.orders] } for order in op.orders]
} }
for op for op

View file

@ -14,6 +14,7 @@
"vite": "^5.0.11" "vite": "^5.0.11"
}, },
"dependencies": { "dependencies": {
"@mantine/charts": "^8.3.13",
"@mantine/core": "^8.3.12", "@mantine/core": "^8.3.12",
"@mantine/dates": "^8.3.13", "@mantine/dates": "^8.3.13",
"@mantine/form": "^8.3.13", "@mantine/form": "^8.3.13",
@ -23,6 +24,7 @@
"react": "^19.2.3", "react": "^19.2.3",
"react-dom": "^19.2.3", "react-dom": "^19.2.3",
"react-router": "^7.12.0", "react-router": "^7.12.0",
"recharts": "^3.8.0",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
} }

View file

@ -113,11 +113,12 @@ export const OrderSet: React.FC = () => {
} }
: { : {
scheduled: false, scheduled: false,
time: [""],
orders: [], orders: [],
}, },
validate: { validate: {
name: (value: string) => name: (value: string) =>
value.length < 1 ? "Please enter a name" : null, !value || value.length < 1 ? "Please enter a name" : null,
time: (value: string, values: FormOrderSet) => time: (value: string, values: FormOrderSet) =>
values.scheduled && (!value || value.length < 1 || !value[0]) values.scheduled && (!value || value.length < 1 || !value[0])
? "Please set a time" ? "Please set a time"
@ -281,7 +282,7 @@ export const OrderSet: React.FC = () => {
label="Time" label="Time"
error={form.getInputProps("time").error} error={form.getInputProps("time").error}
> >
{(form.getValues().time as string[]).map((time, idx) => ( {((form.getValues().time ?? []) as string[]).map((time, idx) => (
<Flex key={idx} gap="xs"> <Flex key={idx} gap="xs">
<TimeInput <TimeInput
key={time} key={time}

View file

@ -17,6 +17,22 @@ import { ConfirmDialogButton } from "./ConfirmDialogButton";
import { NavigateButton } from "./NavigateButton"; import { NavigateButton } from "./NavigateButton";
import { Link, useFetcher } from "react-router"; import { Link, useFetcher } from "react-router";
import { useUserContext } from "./UserContext"; import { useUserContext } from "./UserContext";
import { DonutChart } from "@mantine/charts";
const COLORS_ROTATION = [
"teal",
"pink",
"lime",
"violet",
"orange",
"blue",
"yellow",
"grape",
"green",
"red",
"cyan",
"gray",
];
export interface OrderSetProps { export interface OrderSetProps {
orderSets: (Pick< orderSets: (Pick<
@ -30,7 +46,7 @@ export interface OrderSetProps {
| "orders" | "orders"
| "probability" | "probability"
> & { > & {
orders: Pick<OrderSetOrder, "id" | "name">; orders: Pick<OrderSetOrder, "id" | "name" | "weight">;
punishment_pool_name: string; punishment_pool_name: string;
})[]; })[];
username: string; username: string;
@ -56,15 +72,19 @@ export const OrderSets: React.FC<OrderSetProps> = ({
const [isMastodonSet, setIsMastodonSet] = React.useState(true); const [isMastodonSet, setIsMastodonSet] = React.useState(true);
React.useEffect(() => { React.useEffect(() => {
fetch(`/api/subs/${username}`) if (username) {
.then((response) => response.json()) fetch(`/api/subs/${username}`)
.then((data) => { .then((response) => response.json())
if (!data.mastodon_server || !data.mastodon_username) { .then((data) => {
setIsMastodonSet(false); if (!data.mastodon_server || !data.mastodon_username) {
} setIsMastodonSet(false);
}); }
});
}
}, [username]); }, [username]);
const [portalRef, setPortalRef] = React.useState<HTMLElement | null>();
return ( return (
<> <>
<Box mb="lg"> <Box mb="lg">
@ -92,90 +112,123 @@ export const OrderSets: React.FC<OrderSetProps> = ({
</Alert> </Alert>
</Flex> </Flex>
)} )}
<Flex gap="md" wrap="wrap"> <div
{orderSets ref={(node) => {
? orderSets.map( if (portalRef == null && node != null) {
({ setPortalRef(node);
id, }
name, }}
scheduled, >
orders, <Flex gap="md" wrap="wrap">
time, {orderSets
weekdays, ? orderSets.map(
weekends, ({
probability, id,
punishment_pool_name, name,
}) => ( scheduled,
<Card orders,
key={id} time,
shadow="sm" weekdays,
padding="lg" weekends,
radius="md" probability,
withBorder punishment_pool_name,
bg="gray.2" }) => (
w="400px" <Card
> key={id}
<Flex direction="column" gap="md" h="100%"> shadow="sm"
<Title order={4}>{name}</Title> padding="lg"
{scheduled ? ( radius="md"
<> withBorder
<Flex gap="xs"> bg="gray.2"
<Text>Scheduled:</Text> w="400px"
{time.split(",").map((time) => ( mb="0"
<div key={time}> >
<TimeValue key={time} value={time} format="12h" /> <Flex direction="column" gap="md" h="100%">
</div> <Title order={4}>{name}</Title>
))} {scheduled ? (
</Flex> <>
<Flex gap="md" align="center"> <Flex gap="xs">
{weekdays ? ( <Text>
<Badge color="blue">Weekdays</Badge> <b>Scheduled:</b>
) : null} </Text>
{weekends ? ( {time.split(",").map((time) => (
<Badge color="blue">Weekends</Badge> <div key={time}>
) : null} <TimeValue
<RingProgress key={time}
size={80} value={time}
label={<Text size="xs">{probability * 100}%</Text>} format="12h"
sections={[ />
{ color: "cyan", value: probability * 100 }, </div>
]} ))}
</Flex>
<Flex gap="md" align="center">
{weekdays ? (
<Badge color="blue">Weekdays</Badge>
) : null}
{weekends ? (
<Badge color="blue">Weekends</Badge>
) : null}
<RingProgress
size={30}
thickness={5}
label={
<Text size="xs" ml="lg">
{probability * 100}%
</Text>
}
sections={[
{ color: "cyan", value: probability * 100 },
]}
/>
</Flex>
</>
) : null}
{punishment_pool_name ? (
<Text flex={1}>
<b>Punishments:</b> {punishment_pool_name}
</Text>
) : null}
<Flex justify="end" align="flex-end" gap="md">
{orders.length > 0 ? (
<DonutChart
flex={1}
size={130}
thickness={30}
data={orders.map(({ name, weight }, idx) => ({
name,
value: weight,
color:
COLORS_ROTATION[idx % COLORS_ROTATION.length],
}))}
tooltipDataSource="segment"
/> />
</Flex> ) : null}
</> <ConfirmDialogButton
) : null} buttonColor="red.8"
<Text mb="md" flex={1}> buttonText="Delete"
Orders: {orders.length} text={`Are you sure you want to delete ${name}?`}
<br /> onConfirm={() => handleDelete(id)}
{punishment_pool_name >
? `Punishments: ${punishment_pool_name}` <IconTrash />
: null} </ConfirmDialogButton>
</Text> <NavigateButton to={`/orders/${username}/${id}`}>
<Flex gap="md" justify="end"> <IconPencil style={{ marginRight: "0.5rem" }} />
<ConfirmDialogButton Edit
buttonColor="red.8" </NavigateButton>
buttonText="Delete" </Flex>
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>
</Flex> </Card>
</Card> ),
), )
) : null}
: null} </Flex>
</Flex> </div>
<NavigateButton to={`/orders/${username}/new`}> <Box my="lg">
<IconPlus style={{ marginRight: "0.5rem" }} /> <NavigateButton to={`/orders/${username}/new`}>
New <IconPlus style={{ marginRight: "0.5rem" }} />
</NavigateButton> New
<Box mb="lg"></Box> </NavigateButton>
</Box>
</> </>
); );
}; };

View file

@ -14,6 +14,7 @@ import { Notifications } from "@mantine/notifications";
import "@mantine/core/styles.css"; import "@mantine/core/styles.css";
import "@mantine/dates/styles.css"; import "@mantine/dates/styles.css";
import "@mantine/notifications/styles.css"; import "@mantine/notifications/styles.css";
import "@mantine/charts/styles.css";
import { Dashboard, subsListLoader } from "./Dashboard"; import { Dashboard, subsListLoader } from "./Dashboard";
import { SubOrderSets, subOrderSetsLoader } from "./SubOrderSets"; import { SubOrderSets, subOrderSetsLoader } from "./SubOrderSets";