Skip to content

Commit 7564774

Browse files
committed
feat: add paging
1 parent 2d8d815 commit 7564774

16 files changed

Lines changed: 292 additions & 66 deletions

packages/admin/src/components/KioskListItem.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Stat, StatNumber, useColorModeValue } from '@chakra-ui/react';
2+
import { useNavigate } from 'react-router-dom';
23

34
import { useKioskContext } from '../context/kiosk.context';
45
import { KioskRoles } from '../types/types';
@@ -12,10 +13,14 @@ interface KioskGridItemProps {
1213

1314
export function KioskListItem({ name, id, role }: KioskGridItemProps) {
1415
const { setSelectedKiosk } = useKioskContext();
16+
const navigate = useNavigate();
1517
const color = useColorModeValue('gray.300', 'gray.600');
1618
return (
1719
<Stat
18-
onClick={() => setSelectedKiosk(id)}
20+
onClick={() => {
21+
setSelectedKiosk(id);
22+
navigate('/dashboard');
23+
}}
1924
borderColor={color}
2025
cursor='pointer'
2126
_hover={{ backgroundColor: color }}

packages/admin/src/context/kiosk.context.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ export function KioskProvider() {
5050
.get<Kiosk>(`${ApiPaths.KIOSK}/${kioskId}`)
5151
.then((res) => {
5252
setKiosk(res.data);
53-
navigate(UIPaths.DASHBOARD);
5453
})
5554
.catch((err) => {
5655
if (isAxiosError(err) && err.response?.status === 401) {

packages/admin/src/pages/KioskDashboard.page.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,21 @@ export function KioskDashboardPage() {
103103
</PageSection>
104104
<PageSection>
105105
<Heading size='md'>{l('page.dashboard.enabledWidgets')}</Heading>
106-
{kiosk?.config.widgets.map((w) => <Text key={w.name}>{WidgetDisplay[w.name].name}</Text>)}
106+
{kiosk?.config.pages && kiosk.config.pages.length > 0 ? (
107+
<>
108+
<Text>Oldalak: {kiosk.config.pages.length}</Text>
109+
{kiosk.config.pages.map((p, idx) => (
110+
<Text
111+
key={`page-${p.title || p.widgets?.[0]?.name || 'x'}-${p.durationSec || 10}-${p.widgets.length}`}
112+
>
113+
#{idx + 1}
114+
{p.title ? ` • ${p.title}` : ''}{p.widgets.length} csempe • {p.durationSec || 10}s
115+
</Text>
116+
))}
117+
</>
118+
) : (
119+
kiosk?.config.widgets?.map((w) => <Text key={w.name}>{WidgetDisplay[w.name].name}</Text>)
120+
)}
107121
</PageSection>
108122
{role && role.role >= 2 && (
109123
<PageSection>

packages/admin/src/pages/Meta.page.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ const validationSchema = z.object({
3131
.string({ required_error: l('form.validation.required') })
3232
.regex(/^-?\d+(\.\d+)?$/, { message: l('form.validation.number') }),
3333
}),
34+
pageDurationSec: z
35+
.string()
36+
.optional()
37+
.transform((v) => (v ? Number(v) : undefined))
38+
.refine((v) => (typeof v === 'undefined' ? true : v >= 1), l('form.validation.number')),
3439
});
3540

3641
export function MetaPage() {
@@ -73,6 +78,10 @@ export function MetaPage() {
7378
<Input {...register('name')} />
7479
{Boolean(errors.name) && <FormErrorMessage>{errors.name?.message}</FormErrorMessage>}
7580
</FormControl>
81+
<FormControl>
82+
<FormLabel>Oldal váltás alapértelmezett ideje (mp)</FormLabel>
83+
<Input type='number' min={1} {...register('pageDurationSec')} />
84+
</FormControl>
7685
</VStack>
7786
{isError && <FormErrorMessage>{l('error.save')}</FormErrorMessage>}
7887
</CardBody>

packages/admin/src/pages/WidgetEditPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function WidgetEditPage() {
1111
<Page title={l('title.widgetEdit')}>
1212
<CardBody>
1313
<Wrap gap={3} w='fit-content'>
14-
{kiosk?.config.widgets.map((widget) => <WidgetListItem widget={widget} key={widget.name} />)}
14+
{kiosk?.config.widgets?.map((widget) => <WidgetListItem widget={widget} key={widget.name} />)}
1515
</Wrap>
1616
</CardBody>
1717
</Page>

packages/admin/src/pages/Widgets.page.tsx

Lines changed: 128 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,146 @@
1-
import { Button, ButtonGroup, CardBody, CardFooter, FormErrorMessage } from '@chakra-ui/react';
2-
import { useState } from 'react';
1+
import { Button, ButtonGroup, CardBody, CardFooter, FormErrorMessage, HStack, Input, Text } from '@chakra-ui/react';
2+
import { useEffect, useMemo, useState } from 'react';
3+
import { TbChevronLeft, TbChevronRight, TbCirclePlus, TbCopy, TbTrash } from 'react-icons/tb';
34

45
import { WidgetGrid } from '../components/widget/WidgetGrid';
56
import { useKioskContext } from '../context/kiosk.context';
67
import { Page } from '../layout/Page';
78
import { useSaveKiosk } from '../network/useSaveKiosk.network';
9+
import { WidgetConfig } from '../types/kiosk.types';
810
import { l } from '../utils/language';
911

1012
export function WidgetsPage() {
1113
const { kiosk, update, selectedKioskId } = useKioskContext();
1214
const { isLoading, isError, makeRequest } = useSaveKiosk(selectedKioskId || '');
13-
const [widgets, setWidgets] = useState(kiosk?.config.widgets);
15+
const initialPages = useMemo(() => {
16+
if (kiosk?.config.pages && kiosk.config.pages.length > 0) return kiosk.config.pages;
17+
const widgets = kiosk?.config.widgets || [];
18+
return [{ durationSec: 10, widgets }];
19+
}, [kiosk]);
20+
21+
const [pages, setPages] = useState(initialPages);
22+
const [selected, setSelected] = useState(0);
23+
24+
useEffect(() => {
25+
setPages(initialPages);
26+
setSelected((prev) => Math.min(prev, initialPages.length - 1));
27+
}, [initialPages]);
28+
29+
const setWidgets = (widgets: WidgetConfig[]) => {
30+
pages[selected] = { ...pages[selected], widgets };
31+
setPages([...pages]);
32+
};
33+
34+
const addPage = () => {
35+
setPages([...pages, { durationSec: 10, widgets: [] }]);
36+
setSelected(pages.length);
37+
};
38+
39+
const removePage = () => {
40+
if (pages.length <= 1) return;
41+
const newPages = pages.filter((_, idx) => idx !== selected);
42+
setPages(newPages);
43+
setSelected(Math.max(0, selected - 1));
44+
};
45+
46+
const setDuration = (v: number) => {
47+
pages[selected] = { ...pages[selected], durationSec: v };
48+
setPages([...pages]);
49+
};
50+
51+
const setTitle = (title: string) => {
52+
pages[selected] = { ...pages[selected], title };
53+
setPages([...pages]);
54+
};
55+
56+
const duplicatePage = () => {
57+
const current = pages[selected];
58+
const cloned = {
59+
durationSec: current.durationSec,
60+
title: current.title ? `${current.title} (másolat)` : undefined,
61+
widgets: JSON.parse(JSON.stringify(current.widgets)),
62+
} as typeof current;
63+
const newPages = [...pages.slice(0, selected + 1), cloned, ...pages.slice(selected + 1)];
64+
setPages(newPages);
65+
setSelected(selected + 1);
66+
};
67+
68+
const prevPage = () => setSelected((p) => Math.max(0, p - 1));
69+
const nextPage = () => setSelected((p) => Math.min(pages.length - 1, p + 1));
70+
1471
const onSave = () => {
15-
makeRequest({ widgets }, update);
72+
makeRequest({ pages }, update);
1673
};
1774
return (
1875
<Page title={l('title.widgets')} isLoading={isLoading}>
1976
<CardBody>
20-
<WidgetGrid widgets={widgets || []} onChange={setWidgets} />
77+
<HStack justifyContent='space-between' mb={3} flexWrap='wrap'>
78+
<HStack>
79+
<Button size='sm' leftIcon={<TbCirclePlus />} onClick={addPage}>
80+
{l('button.add')} oldal
81+
</Button>
82+
<Button
83+
size='sm'
84+
colorScheme='red'
85+
leftIcon={<TbTrash />}
86+
onClick={removePage}
87+
isDisabled={pages.length <= 1}
88+
>
89+
Oldal törlése
90+
</Button>
91+
<Button size='sm' leftIcon={<TbCopy />} onClick={duplicatePage} isDisabled={pages.length < 1}>
92+
Oldal duplikálása
93+
</Button>
94+
</HStack>
95+
<HStack>
96+
<Button
97+
size='sm'
98+
variant='outline'
99+
onClick={prevPage}
100+
leftIcon={<TbChevronLeft />}
101+
isDisabled={selected === 0}
102+
>
103+
Előző
104+
</Button>
105+
<Text>Oldal:</Text>
106+
<Input
107+
type='number'
108+
min={1}
109+
max={pages.length}
110+
value={selected + 1}
111+
onChange={(e) => {
112+
const v = parseInt(e.target.value, 10);
113+
if (!Number.isNaN(v)) setSelected(Math.min(Math.max(0, v - 1), pages.length - 1));
114+
}}
115+
width='4rem'
116+
/>
117+
<Button
118+
size='sm'
119+
variant='outline'
120+
onClick={nextPage}
121+
rightIcon={<TbChevronRight />}
122+
isDisabled={selected >= pages.length - 1}
123+
>
124+
Következő
125+
</Button>
126+
<Text>Időtartam (s):</Text>
127+
<Input
128+
type='number'
129+
min={1}
130+
value={pages[selected]?.durationSec || 10}
131+
onChange={(e) => setDuration(Math.max(1, parseInt(e.target.value || '10', 10)))}
132+
width='6rem'
133+
/>
134+
<Text>Cím:</Text>
135+
<Input
136+
placeholder='Oldal címe'
137+
value={pages[selected]?.title || ''}
138+
onChange={(e) => setTitle(e.target.value)}
139+
width='12rem'
140+
/>
141+
</HStack>
142+
</HStack>
143+
<WidgetGrid widgets={pages[selected]?.widgets || []} onChange={setWidgets} />
21144
{isError && <FormErrorMessage>{l('error.save')}</FormErrorMessage>}
22145
</CardBody>
23146
<CardFooter>

packages/admin/src/types/kiosk.types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ export enum KioskStatus {
3636
export type KioskConfig = {
3737
style: Style;
3838
meta: Meta;
39-
widgets: WidgetConfig[];
39+
// Backward compatibility: single-page widgets
40+
widgets?: WidgetConfig[];
41+
pages?: Page[];
4042
};
4143

4244
export type Coordinates = {
@@ -47,6 +49,7 @@ export type Coordinates = {
4749
export type Meta = {
4850
coordinates: Coordinates;
4951
name: string;
52+
pageDurationSec?: number;
5053
};
5154

5255
export type Style = {
@@ -69,6 +72,13 @@ export type ColorModeColor = {
6972
dark: string;
7073
};
7174

75+
export type Page = {
76+
// Duration for this page in seconds
77+
durationSec?: number;
78+
title?: string;
79+
widgets: WidgetConfig[];
80+
};
81+
7282
export type WidgetName =
7383
| 'weather'
7484
| 'schpincer'

packages/admin/src/types/types.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export type RegistrationForm = {
2222
export type MetaForm = {
2323
name: string;
2424
coordinates: Coordinates;
25+
pageDurationSec?: number | string;
2526
};
2627

2728
export type NotificationForm = Omit<KioskNotification, 'status'>;

packages/backend/src/kiosk/kiosk.service.ts

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,14 @@ export class KioskService {
3030
});
3131
}
3232

33-
async patchKiosk(kioskId: string, { widgets, style, meta }: KioskPatchDto) {
34-
const kiosk = await this.kioskModel.findById(kioskId);
35-
if (typeof widgets !== 'undefined') {
36-
kiosk.config.widgets = widgets;
37-
}
38-
if (typeof meta !== 'undefined') {
39-
kiosk.config.meta = meta;
40-
}
41-
if (typeof style !== 'undefined') {
42-
kiosk.config.style = style;
43-
}
44-
return this.kioskModel.updateOne(
45-
{ _id: kioskId },
46-
{
47-
$set: {
48-
config: kiosk.config,
49-
},
50-
}
51-
);
33+
async patchKiosk(kioskId: string, { widgets, style, meta, pages }: KioskPatchDto) {
34+
const $set: Record<string, unknown> = {};
35+
if (typeof widgets !== 'undefined') $set['config.widgets'] = widgets;
36+
if (typeof pages !== 'undefined') $set['config.pages'] = pages;
37+
if (typeof meta !== 'undefined') $set['config.meta'] = meta;
38+
if (typeof style !== 'undefined') $set['config.style'] = style;
39+
if (Object.keys($set).length === 0) return { acknowledged: true, matchedCount: 0, modifiedCount: 0 } as never;
40+
return this.kioskModel.updateOne({ _id: kioskId }, { $set });
5241
}
5342

5443
async patchWidget(kioskId: string, widget: WidgetPatchDto) {

packages/backend/src/types/kiosk.types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
export type KioskConfig = {
22
style: Style;
33
meta: Meta;
4-
widgets: WidgetConfig[];
4+
// Backward compatibility: single-page widgets
5+
widgets?: WidgetConfig[];
6+
pages?: Page[];
57
};
68

79
export type KioskNotification = {
@@ -31,6 +33,7 @@ export type Coordinates = {
3133
export type Meta = {
3234
coordinates: Coordinates;
3335
name: string;
36+
pageDurationSec?: number;
3437
};
3538

3639
export type Style = {
@@ -53,6 +56,13 @@ export type ColorModeColor = {
5356
dark: string;
5457
};
5558

59+
export type Page = {
60+
// Duration for this page in seconds
61+
durationSec?: number;
62+
title?: string;
63+
widgets: WidgetConfig[];
64+
};
65+
5666
export type WidgetName =
5767
| 'weather'
5868
| 'schpincer'

0 commit comments

Comments
 (0)