Skip to content

Commit 30ed659

Browse files
committed
balloon complete
1 parent 324e571 commit 30ed659

File tree

3 files changed

+216
-6
lines changed

3 files changed

+216
-6
lines changed

packages/ui/app/components/BalloonsModal.tsx

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,104 @@
11
import React, { useState } from 'react';
22
import {
3-
Button, ColorInput, Fieldset, Modal, Text,
3+
Accordion,
4+
ActionIcon,
5+
Button, Center, ColorInput, Fieldset, FocusTrap, Group, LoadingOverlay, Modal, Text,
6+
TextInput, ThemeIcon, Tooltip,
47
} from '@mantine/core';
58
import { useDisclosure } from '@mantine/hooks';
9+
import { notifications } from '@mantine/notifications';
10+
import { IconCheck, IconCopy, IconX } from '@tabler/icons-react';
611
import { convertToChinese, convertToColor } from '../utils';
712

13+
export function BalloonsClient({ clients, refresh }) {
14+
const [adding, setAdding] = useState(false);
15+
const [opened, { open, close }] = useDisclosure(false);
16+
const [name, setName] = useState('');
17+
18+
const addClient = async () => {
19+
setAdding(true);
20+
try {
21+
const res = await (await fetch('/client', {
22+
method: 'POST',
23+
headers: { 'Content-Type': 'application/json' },
24+
body: JSON.stringify({ name, operation: 'add', type: 'balloon' }),
25+
})).json();
26+
if (res.error) {
27+
notifications.show({ title: 'Error', message: `${res.error.message}(${res.error.params})`, color: 'red' });
28+
setAdding(false);
29+
return;
30+
}
31+
notifications.show({ title: 'Success', message: 'Client added', color: 'green' });
32+
setName('');
33+
} catch (e) {
34+
console.error(e);
35+
notifications.show({ title: 'Error', message: 'Failed to add client', color: 'red' });
36+
}
37+
setAdding(false);
38+
close();
39+
refresh();
40+
};
41+
return (
42+
<>
43+
<Modal
44+
opened={opened}
45+
onClose={() => { close(); setName(''); }}
46+
title="Clients"
47+
size="md"
48+
padding="md"
49+
>
50+
<Fieldset legend="Add Client" mb="lg">
51+
<LoadingOverlay visible={adding} zIndex={1000} overlayProps={{ radius: 'sm', blur: 2 }} />
52+
<FocusTrap active>
53+
<TextInput label="Client Name" placeholder="Client Name" value={name} onChange={(e) => setName(e.currentTarget.value)} data-autofocus />
54+
</FocusTrap>
55+
<Button color="blue" fullWidth mt="md" radius="md" onClick={addClient}>Submit</Button>
56+
</Fieldset>
57+
<Fieldset legend="Clients" mb="lg">
58+
<Accordion>
59+
{clients.map((item) => (
60+
<Accordion.Item key={item.id} value={item.name}>
61+
<Accordion.Control
62+
icon={(<Tooltip label={item.updateAt && item.updateAt > new Date().getTime() - 1000 * 60 ? 'Online' : 'Offline'}>
63+
<ThemeIcon radius="xl" size="sm" color={item.updateAt ? 'green' : 'red'}>
64+
{ item.updateAt ? (<IconCheck />) : (<IconX />)}
65+
</ThemeIcon>
66+
</Tooltip>)}
67+
>
68+
<Tooltip label={`${item.name}(${item.id})`}>
69+
<Text>{item.name}({item.id})</Text>
70+
</Tooltip>
71+
</Accordion.Control>
72+
<Accordion.Panel>
73+
<Group justify="center" gap="md">
74+
<Text>ID: {item.id}</Text>
75+
<Tooltip label="Copy ID">
76+
<ActionIcon variant="transparent" color="blue" aria-label='Copy ID' ml="xs" onClick={() => {
77+
notifications.show({ title: 'Success', message: 'ID Copied to clipboard!', color: 'green' });
78+
}}><IconCopy /></ActionIcon>
79+
</Tooltip>
80+
</Group>
81+
{ !item.updateAt ? (
82+
<Center mt="md">
83+
<Text c="dimmed">Have not connected yet</Text>
84+
</Center>
85+
) : (
86+
<>
87+
<Text>IP: {item.ip}</Text>
88+
<Text>Updated At: {new Date(item.updateAt).toLocaleString()}</Text>
89+
</>
90+
)}
91+
</Accordion.Panel>
92+
</Accordion.Item>
93+
))}
94+
</Accordion>
95+
</Fieldset>
96+
</Modal>
97+
<Button color="blue" radius="md" onClick={open}>Client Info</Button>
98+
</>
99+
);
100+
}
101+
8102
export function BallonColorChecker() {
9103
const [opened, { open, close }] = useDisclosure(false);
10104
const [value, setValue] = useState('');
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React from 'react';
2+
import {
3+
ActionIcon, Badge, Group, LoadingOverlay, Table,
4+
ThemeIcon, Tooltip,
5+
} from '@mantine/core';
6+
import { notifications } from '@mantine/notifications';
7+
import {
8+
IconCheck, IconHourglassEmpty, IconPrinter, IconRefresh,
9+
} from '@tabler/icons-react';
10+
11+
function BalloonRow({ balloon, refresh }) {
12+
const [loading, setLoading] = React.useState(false);
13+
14+
const actions = async (balloonid, operation) => {
15+
setLoading(true);
16+
try {
17+
const res = await (await fetch('/balloon', {
18+
method: 'POST',
19+
headers: { 'Content-Type': 'application/json' },
20+
body: JSON.stringify({ balloonid, operation }),
21+
})).json();
22+
if (res.error) {
23+
notifications.show({ title: 'Error', message: `${res.error.message}(${res.error.params})`, color: 'red' });
24+
return;
25+
}
26+
notifications.show({ title: 'Success', message: 'Balloon Updated', color: 'green' });
27+
} catch (e) {
28+
console.error(e);
29+
notifications.show({ title: 'Error', message: 'Failed to update balloon', color: 'red' });
30+
}
31+
setLoading(false);
32+
refresh();
33+
};
34+
35+
return (
36+
<Table.Tr key={balloon._id}>
37+
<Table.Td>
38+
<ThemeIcon radius="xl" size="sm" color={balloon.printDone ? 'green' : balloon.receivedAt ? 'blue' : 'gray'}>
39+
{ balloon.printDone ? <IconCheck /> : balloon.receivedAt ? <IconPrinter /> : <IconHourglassEmpty /> }
40+
</ThemeIcon>
41+
</Table.Td>
42+
<Table.Td>{ balloon.balloonid }</Table.Td>
43+
<Table.Td>{ new Date(+balloon.time).toLocaleTimeString() }</Table.Td>
44+
<Table.Td><Badge color={balloon.contestproblem.rgb} size="xl" radius="sm">{ balloon.problem }</Badge></Table.Td>
45+
<Table.Td>{ balloon.team }</Table.Td>
46+
<Table.Td>{ balloon.affiliation }</Table.Td>
47+
<Table.Td>{ balloon.location }</Table.Td>
48+
<Table.Td style={{ minWidth: 50 }}>{ balloon.awards }</Table.Td>
49+
<Table.Td>
50+
{ (Object.values(balloon.total) as any[]).map((t) => (
51+
<Badge key={t.short_name} color={t.rgb} size="md" radius="sm">{ t.short_name }</Badge>
52+
)) }
53+
</Table.Td>
54+
<Table.Td style={{ minWidth: 50 }}>
55+
<LoadingOverlay visible={loading} zIndex={1000} overlayProps={{ radius: 'sm', blur: 2 }} />
56+
<Group justify="center" gap={5}>
57+
<Tooltip label="Reprint">
58+
<ActionIcon variant="transparent" color="yellow" aria-label='Reprint' onClick={
59+
() => actions(balloon.balloonid, 'reprint')
60+
}><IconRefresh /></ActionIcon>
61+
</Tooltip>
62+
</Group>
63+
</Table.Td>
64+
</Table.Tr>
65+
);
66+
}
67+
68+
export function BalloonsTable({ balloons, refresh }) {
69+
return (
70+
<Table
71+
horizontalSpacing="md" verticalSpacing="xs" miw={700}
72+
striped highlightOnHover stickyHeader
73+
>
74+
<Table.Thead>
75+
<Table.Tr>
76+
<Table.Th></Table.Th>
77+
<Table.Th>#</Table.Th>
78+
<Table.Th>Time</Table.Th>
79+
<Table.Th>Solved</Table.Th>
80+
<Table.Th>Team</Table.Th>
81+
<Table.Th>Affiliation</Table.Th>
82+
<Table.Th>location</Table.Th>
83+
<Table.Th>Awards</Table.Th>
84+
<Table.Th>Total</Table.Th>
85+
<Table.Th>Actions</Table.Th>
86+
</Table.Tr>
87+
</Table.Thead>
88+
<Table.Tbody>{ balloons.map((balloon) => <BalloonRow key={balloon._id} balloon={balloon} refresh={refresh} />) }</Table.Tbody>
89+
</Table>
90+
);
91+
}

packages/ui/app/pages/Balloon.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,35 @@
11
import React from 'react';
2+
import {
3+
Card, Center, Group, LoadingOverlay, Text, Title,
4+
} from '@mantine/core';
5+
import { useQuery } from '@tanstack/react-query';
6+
import { BallonColorChecker, BalloonsClient } from '../components/BalloonsModal';
7+
import { BalloonsTable } from '../components/BalloonsTable';
8+
9+
export default function Print() {
10+
const query = useQuery({
11+
queryKey: ['balloons'],
12+
queryFn: () => fetch('/balloon').then((res) => res.json()),
13+
refetchInterval: 300000,
14+
});
15+
16+
const load = query.isLoading || query.isFetching || query.isRefetching;
217

3-
export default function Balloon() {
418
return (
5-
<div>
6-
<h1>Balloons</h1>
7-
<p>Balloons</p>
8-
</div>
19+
<Card shadow="sm" padding="lg" radius="md" withBorder>
20+
<LoadingOverlay visible={load} zIndex={1000} />
21+
<Group justify="space-between" mb="xs">
22+
<Title order={3}>Balloons</Title>
23+
<Group>
24+
<BallonColorChecker />
25+
<BalloonsClient clients={query.data?.clients || []} refresh={query.refetch} />
26+
</Group>
27+
</Group>
28+
{ !load && (!(query.data?.balloons || []).length ? (
29+
<Center mt="md">
30+
<Text c="dimmed">No balloons found</Text>
31+
</Center>
32+
) : (<BalloonsTable balloons={query.data?.balloons || []} refresh={query.refetch} />))}
33+
</Card>
934
);
1035
}

0 commit comments

Comments
 (0)