Skip to content

Commit 630aedc

Browse files
committed
feat: Product selection UI for sync
1 parent 00bb9fa commit 630aedc

8 files changed

Lines changed: 307 additions & 78 deletions

File tree

Lines changed: 122 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,137 @@
11
"use client";
22

33
import { loadSalesFromEActivites } from "@/lib/crud/loadSalesFromEActivites";
4-
import { Button } from "@mantine/core";
5-
import React, { useTransition } from "react";
4+
import { fetcher } from "@/lib/fetcher";
5+
import { StatusReturn } from "@/lib/types";
6+
import { Product } from "@docsoc/eactivities";
7+
import { Alert, Box, Button, Checkbox, Group, Loader, Modal, Stack, Text } from "@mantine/core";
8+
import { useDisclosure } from "@mantine/hooks";
9+
import { RootItem } from "@prisma/client";
10+
import React, { useCallback, useState, useTransition } from "react";
611
import { FaSync } from "react-icons/fa";
12+
import { FaUpRightFromSquare } from "react-icons/fa6";
13+
import useSWR from "swr";
14+
15+
function CheckboxesForProducts({ close }: { close: () => void }) {
16+
const [value, setValue] = useState<string[]>([]);
17+
const [status, setStatus] = useState<StatusReturn>({
18+
status: "pending",
19+
});
20+
21+
const { data: products, isLoading } = useSWR<RootItem[]>("/api/products/syncable", fetcher);
722

8-
export const SyncEActivities = ({
9-
setActionsError,
10-
}: {
11-
setActionsError: (error: string | null) => void;
12-
}) => {
1323
const [isPending, startTransition] = useTransition();
1424

15-
const syncEActivities = () => {
16-
setActionsError(null);
25+
const syncEActivities = useCallback(() => {
26+
setStatus({
27+
status: "pending",
28+
});
1729
startTransition(async () => {
18-
const res = await loadSalesFromEActivites();
30+
const res = await loadSalesFromEActivites(
31+
value.map((id) => parseInt(id, 10)).filter(isFinite),
32+
);
1933
if (res.status === "error") {
20-
setActionsError(res.error);
34+
setStatus(res);
35+
} else {
36+
setStatus({
37+
status: "success",
38+
});
39+
close();
2140
}
2241
});
23-
};
42+
}, [close, value]);
43+
44+
const cards = products?.map((product, i) => (
45+
<Checkbox.Card radius="md" value={product.id.toString(10)} key={i} flex="1 0 0">
46+
<Group wrap="nowrap" align="center" p="md">
47+
<Checkbox.Indicator />
48+
<Group flex="1 0 0">
49+
<Box flex="1 0 0">
50+
<Text>
51+
<b>
52+
<u>{product.name}</u>
53+
</b>
54+
</Text>
55+
<Text>
56+
<b>eActivites ID:</b> {product.eActivitiesId}
57+
</Text>
58+
<Text>
59+
<b>eActivites Name:</b> {product.eActivitiesName}
60+
</Text>
61+
</Box>
62+
{isLoading ? (
63+
<Loader size="md" />
64+
) : (
65+
product.eActivitiesURL && (
66+
<Button
67+
component="a"
68+
href={product.eActivitiesURL}
69+
target="_blank"
70+
rightSection={<FaUpRightFromSquare />}
71+
>
72+
View on Union Shop
73+
</Button>
74+
)
75+
)}
76+
</Group>
77+
</Group>
78+
</Checkbox.Card>
79+
));
80+
81+
return (
82+
<Stack>
83+
{
84+
// Display error if there is one
85+
status.status === "error" && (
86+
<Alert color="red" title="Error">
87+
{status.error}
88+
</Alert>
89+
)
90+
}
91+
<Checkbox.Group
92+
value={value}
93+
onChange={setValue}
94+
label="Select products to sync"
95+
description="If you are being IP banned, you might want to wait a few minutes and try fewer products"
96+
>
97+
<Stack pt="md" gap="xs">
98+
{products && products.length > 0 ? cards : <Text>No products found</Text>}
99+
</Stack>
100+
</Checkbox.Group>
101+
102+
<Group justify="space-between" mt="sm">
103+
<Button
104+
onClick={() =>
105+
setValue(products?.map((product) => product.id.toString(10)) ?? [])
106+
}
107+
color="violet"
108+
loading={false}
109+
>
110+
Select all
111+
</Button>
112+
<Button onClick={syncEActivities} color="green" loading={isPending}>
113+
Sync {value.length} products
114+
</Button>
115+
</Group>
116+
</Stack>
117+
);
118+
}
119+
120+
export const SyncEActivities = ({
121+
setActionsError,
122+
}: {
123+
setActionsError: (error: string | null) => void;
124+
}) => {
125+
const [opened, { open, close }] = useDisclosure(false);
24126

25127
return (
26-
<Button
27-
leftSection={<FaSync />}
28-
color="violet"
29-
loading={isPending}
30-
onClick={syncEActivities}
31-
>
32-
Sync from eActivities
33-
</Button>
128+
<>
129+
<Modal opened={opened} onClose={close} title={`Select products to sync`} size="xl">
130+
<CheckboxesForProducts close={close} />
131+
</Modal>
132+
<Button leftSection={<FaSync />} color="violet" onClick={open}>
133+
Sync from eActivities
134+
</Button>
135+
</>
34136
);
35137
};

collection/app/(app)/products/MapProduct.tsx

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,30 @@
11
"use client";
22

3+
import { ProductSelectionCard } from "@/components/ProductSelectionCard";
34
import {
4-
addProducts,
55
ProductsAndVariantsByAcademicYear,
66
updateProductWithEActivitesMetadata,
77
} from "@/lib/crud/products";
8-
import { fetcher } from "@/lib/fetcher";
98
import { StatusReturn } from "@/lib/types";
109
import { Product } from "@docsoc/eactivities";
1110
import {
1211
ActionIcon,
1312
Alert,
1413
Button,
1514
Group,
16-
InputLabel,
1715
Modal,
1816
NativeSelect,
1917
Radio,
2018
Stack,
21-
TextInput,
2219
Tooltip,
2320
Text,
24-
Anchor,
2521
Box,
2622
Loader,
2723
} from "@mantine/core";
28-
import { useForm } from "@mantine/form";
2924
import { useDisclosure } from "@mantine/hooks";
3025
import React, { useEffect, useState, useTransition } from "react";
3126
import { FaEdit } from "react-icons/fa";
32-
import { FaPlus, FaSignsPost, FaTrash, FaUpRightFromSquare } from "react-icons/fa6";
33-
import useSWR from "swr";
27+
import { FaSignsPost, FaUpRightFromSquare } from "react-icons/fa6";
3428

3529
interface MapProductProps {
3630
academicYears: string[];
@@ -111,35 +105,12 @@ const MapProductForm: React.FC<MapProductFormProps> = ({
111105
}, [academicYear]);
112106

113107
const cards = products.map((product) => (
114-
<Radio.Card radius="md" key={product.ID} value={product.ID.toString(10)} flex="1 0 0">
115-
<Group wrap="nowrap" align="center" p="md">
116-
<Radio.Indicator />
117-
<Group flex="1 0 0">
118-
<Box flex="1 0 0">
119-
<Text>
120-
<b>
121-
<u>{product.Name}</u>
122-
</b>
123-
</Text>
124-
<Text>
125-
<b>ID:</b> {product.ID}
126-
</Text>
127-
<Text>
128-
<b>Variants:</b>{" "}
129-
{product.ProductLines?.map((line) => line.Name).join(" | ")}
130-
</Text>
131-
</Box>
132-
<Button
133-
component="a"
134-
href={product.URL}
135-
target="_blank"
136-
rightSection={<FaUpRightFromSquare />}
137-
>
138-
View on Union Shop
139-
</Button>
140-
</Group>
141-
</Group>
142-
</Radio.Card>
108+
<ProductSelectionCard
109+
product={product}
110+
key={product.ID}
111+
cardParent={Radio.Card}
112+
indicator={Radio.Indicator}
113+
/>
143114
));
144115

145116
return (
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Returns just those products we can sync from eActivities
3+
*/
4+
import { auth } from "@/auth";
5+
import { getSyncableProducts } from "@/lib/crud/products";
6+
import { NextResponse } from "next/server";
7+
8+
export const GET = auth(async function GET(request) {
9+
if (!request.auth) {
10+
return NextResponse.json(
11+
{
12+
message: "Unauthorized",
13+
},
14+
{ status: 401 },
15+
);
16+
}
17+
return NextResponse.json(await getSyncableProducts());
18+
});
19+
20+
export const dynamic = "force-dynamic";
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Product } from "@docsoc/eactivities";
2+
import { Checkbox, Group, Radio, Text, Box, Button } from "@mantine/core";
3+
import React from "react";
4+
import { FaUpRightFromSquare } from "react-icons/fa6";
5+
6+
interface ProductSelectionCardProps {
7+
cardParent: typeof Radio.Card | typeof Checkbox.Card;
8+
indicator: typeof Radio.Indicator | typeof Checkbox.Indicator;
9+
product: Product;
10+
}
11+
12+
export const ProductSelectionCard: React.FC<ProductSelectionCardProps> = ({
13+
cardParent: CardParent,
14+
indicator: Indicator,
15+
product,
16+
}) => {
17+
return (
18+
<CardParent radius="md" key={product.ID} value={product.ID.toString(10)} flex="1 0 0">
19+
<Group wrap="nowrap" align="center" p="md">
20+
<Indicator />
21+
<Group flex="1 0 0">
22+
<Box flex="1 0 0">
23+
<Text>
24+
<b>
25+
<u>{product.Name}</u>
26+
</b>
27+
</Text>
28+
<Text>
29+
<b>ID:</b> {product.ID}
30+
</Text>
31+
<Text>
32+
<b>Variants:</b>{" "}
33+
{product.ProductLines?.map((line) => line.Name).join(" | ")}
34+
</Text>
35+
</Box>
36+
<Button
37+
component="a"
38+
href={product.URL}
39+
target="_blank"
40+
rightSection={<FaUpRightFromSquare />}
41+
>
42+
View on Union Shop
43+
</Button>
44+
</Group>
45+
</Group>
46+
</CardParent>
47+
);
48+
};

0 commit comments

Comments
 (0)