From 75647743a6607ca23e8ec7523a81b059ba232af4 Mon Sep 17 00:00:00 2001 From: Gyula Kiri Date: Thu, 11 Sep 2025 18:39:56 +0200 Subject: [PATCH 1/4] feat: add paging --- .../admin/src/components/KioskListItem.tsx | 7 +- packages/admin/src/context/kiosk.context.tsx | 1 - .../admin/src/pages/KioskDashboard.page.tsx | 16 ++- packages/admin/src/pages/Meta.page.tsx | 9 ++ packages/admin/src/pages/WidgetEditPage.tsx | 2 +- packages/admin/src/pages/Widgets.page.tsx | 133 +++++++++++++++++- packages/admin/src/types/kiosk.types.ts | 12 +- packages/admin/src/types/types.tsx | 1 + packages/backend/src/kiosk/kiosk.service.ts | 27 ++-- packages/backend/src/types/kiosk.types.ts | 12 +- packages/client/src/App.tsx | 9 +- .../src/layout/ActiveWidgetConfigContext.tsx | 13 ++ packages/client/src/layout/ConfigContext.tsx | 40 +++++- .../client/src/layout/WidgetDistributor.tsx | 54 +++---- packages/client/src/types/config.type.ts | 12 +- packages/client/src/utils/useWidgetConfig.ts | 10 +- 16 files changed, 292 insertions(+), 66 deletions(-) create mode 100644 packages/client/src/layout/ActiveWidgetConfigContext.tsx diff --git a/packages/admin/src/components/KioskListItem.tsx b/packages/admin/src/components/KioskListItem.tsx index df7e597..3ec8112 100644 --- a/packages/admin/src/components/KioskListItem.tsx +++ b/packages/admin/src/components/KioskListItem.tsx @@ -1,4 +1,5 @@ import { Stat, StatNumber, useColorModeValue } from '@chakra-ui/react'; +import { useNavigate } from 'react-router-dom'; import { useKioskContext } from '../context/kiosk.context'; import { KioskRoles } from '../types/types'; @@ -12,10 +13,14 @@ interface KioskGridItemProps { export function KioskListItem({ name, id, role }: KioskGridItemProps) { const { setSelectedKiosk } = useKioskContext(); + const navigate = useNavigate(); const color = useColorModeValue('gray.300', 'gray.600'); return ( setSelectedKiosk(id)} + onClick={() => { + setSelectedKiosk(id); + navigate('/dashboard'); + }} borderColor={color} cursor='pointer' _hover={{ backgroundColor: color }} diff --git a/packages/admin/src/context/kiosk.context.tsx b/packages/admin/src/context/kiosk.context.tsx index b9a2a60..974f045 100644 --- a/packages/admin/src/context/kiosk.context.tsx +++ b/packages/admin/src/context/kiosk.context.tsx @@ -50,7 +50,6 @@ export function KioskProvider() { .get(`${ApiPaths.KIOSK}/${kioskId}`) .then((res) => { setKiosk(res.data); - navigate(UIPaths.DASHBOARD); }) .catch((err) => { if (isAxiosError(err) && err.response?.status === 401) { diff --git a/packages/admin/src/pages/KioskDashboard.page.tsx b/packages/admin/src/pages/KioskDashboard.page.tsx index cf7cf8c..bc404d4 100644 --- a/packages/admin/src/pages/KioskDashboard.page.tsx +++ b/packages/admin/src/pages/KioskDashboard.page.tsx @@ -103,7 +103,21 @@ export function KioskDashboardPage() { {l('page.dashboard.enabledWidgets')} - {kiosk?.config.widgets.map((w) => {WidgetDisplay[w.name].name})} + {kiosk?.config.pages && kiosk.config.pages.length > 0 ? ( + <> + Oldalak: {kiosk.config.pages.length} + {kiosk.config.pages.map((p, idx) => ( + + #{idx + 1} + {p.title ? ` • ${p.title}` : ''} • {p.widgets.length} csempe • {p.durationSec || 10}s + + ))} + + ) : ( + kiosk?.config.widgets?.map((w) => {WidgetDisplay[w.name].name}) + )} {role && role.role >= 2 && ( diff --git a/packages/admin/src/pages/Meta.page.tsx b/packages/admin/src/pages/Meta.page.tsx index de79d84..74c8338 100644 --- a/packages/admin/src/pages/Meta.page.tsx +++ b/packages/admin/src/pages/Meta.page.tsx @@ -31,6 +31,11 @@ const validationSchema = z.object({ .string({ required_error: l('form.validation.required') }) .regex(/^-?\d+(\.\d+)?$/, { message: l('form.validation.number') }), }), + pageDurationSec: z + .string() + .optional() + .transform((v) => (v ? Number(v) : undefined)) + .refine((v) => (typeof v === 'undefined' ? true : v >= 1), l('form.validation.number')), }); export function MetaPage() { @@ -73,6 +78,10 @@ export function MetaPage() { {Boolean(errors.name) && {errors.name?.message}} + + Oldal váltás alapértelmezett ideje (mp) + + {isError && {l('error.save')}} diff --git a/packages/admin/src/pages/WidgetEditPage.tsx b/packages/admin/src/pages/WidgetEditPage.tsx index 637574e..5abc205 100644 --- a/packages/admin/src/pages/WidgetEditPage.tsx +++ b/packages/admin/src/pages/WidgetEditPage.tsx @@ -11,7 +11,7 @@ export function WidgetEditPage() { - {kiosk?.config.widgets.map((widget) => )} + {kiosk?.config.widgets?.map((widget) => )} diff --git a/packages/admin/src/pages/Widgets.page.tsx b/packages/admin/src/pages/Widgets.page.tsx index e2db6a4..1255912 100644 --- a/packages/admin/src/pages/Widgets.page.tsx +++ b/packages/admin/src/pages/Widgets.page.tsx @@ -1,23 +1,146 @@ -import { Button, ButtonGroup, CardBody, CardFooter, FormErrorMessage } from '@chakra-ui/react'; -import { useState } from 'react'; +import { Button, ButtonGroup, CardBody, CardFooter, FormErrorMessage, HStack, Input, Text } from '@chakra-ui/react'; +import { useEffect, useMemo, useState } from 'react'; +import { TbChevronLeft, TbChevronRight, TbCirclePlus, TbCopy, TbTrash } from 'react-icons/tb'; import { WidgetGrid } from '../components/widget/WidgetGrid'; import { useKioskContext } from '../context/kiosk.context'; import { Page } from '../layout/Page'; import { useSaveKiosk } from '../network/useSaveKiosk.network'; +import { WidgetConfig } from '../types/kiosk.types'; import { l } from '../utils/language'; export function WidgetsPage() { const { kiosk, update, selectedKioskId } = useKioskContext(); const { isLoading, isError, makeRequest } = useSaveKiosk(selectedKioskId || ''); - const [widgets, setWidgets] = useState(kiosk?.config.widgets); + const initialPages = useMemo(() => { + if (kiosk?.config.pages && kiosk.config.pages.length > 0) return kiosk.config.pages; + const widgets = kiosk?.config.widgets || []; + return [{ durationSec: 10, widgets }]; + }, [kiosk]); + + const [pages, setPages] = useState(initialPages); + const [selected, setSelected] = useState(0); + + useEffect(() => { + setPages(initialPages); + setSelected((prev) => Math.min(prev, initialPages.length - 1)); + }, [initialPages]); + + const setWidgets = (widgets: WidgetConfig[]) => { + pages[selected] = { ...pages[selected], widgets }; + setPages([...pages]); + }; + + const addPage = () => { + setPages([...pages, { durationSec: 10, widgets: [] }]); + setSelected(pages.length); + }; + + const removePage = () => { + if (pages.length <= 1) return; + const newPages = pages.filter((_, idx) => idx !== selected); + setPages(newPages); + setSelected(Math.max(0, selected - 1)); + }; + + const setDuration = (v: number) => { + pages[selected] = { ...pages[selected], durationSec: v }; + setPages([...pages]); + }; + + const setTitle = (title: string) => { + pages[selected] = { ...pages[selected], title }; + setPages([...pages]); + }; + + const duplicatePage = () => { + const current = pages[selected]; + const cloned = { + durationSec: current.durationSec, + title: current.title ? `${current.title} (másolat)` : undefined, + widgets: JSON.parse(JSON.stringify(current.widgets)), + } as typeof current; + const newPages = [...pages.slice(0, selected + 1), cloned, ...pages.slice(selected + 1)]; + setPages(newPages); + setSelected(selected + 1); + }; + + const prevPage = () => setSelected((p) => Math.max(0, p - 1)); + const nextPage = () => setSelected((p) => Math.min(pages.length - 1, p + 1)); + const onSave = () => { - makeRequest({ widgets }, update); + makeRequest({ pages }, update); }; return ( - + + + + + + + + + Oldal: + { + const v = parseInt(e.target.value, 10); + if (!Number.isNaN(v)) setSelected(Math.min(Math.max(0, v - 1), pages.length - 1)); + }} + width='4rem' + /> + + Időtartam (s): + setDuration(Math.max(1, parseInt(e.target.value || '10', 10)))} + width='6rem' + /> + Cím: + setTitle(e.target.value)} + width='12rem' + /> + + + {isError && {l('error.save')}} diff --git a/packages/admin/src/types/kiosk.types.ts b/packages/admin/src/types/kiosk.types.ts index 3a11fb0..81b0fa9 100644 --- a/packages/admin/src/types/kiosk.types.ts +++ b/packages/admin/src/types/kiosk.types.ts @@ -36,7 +36,9 @@ export enum KioskStatus { export type KioskConfig = { style: Style; meta: Meta; - widgets: WidgetConfig[]; + // Backward compatibility: single-page widgets + widgets?: WidgetConfig[]; + pages?: Page[]; }; export type Coordinates = { @@ -47,6 +49,7 @@ export type Coordinates = { export type Meta = { coordinates: Coordinates; name: string; + pageDurationSec?: number; }; export type Style = { @@ -69,6 +72,13 @@ export type ColorModeColor = { dark: string; }; +export type Page = { + // Duration for this page in seconds + durationSec?: number; + title?: string; + widgets: WidgetConfig[]; +}; + export type WidgetName = | 'weather' | 'schpincer' diff --git a/packages/admin/src/types/types.tsx b/packages/admin/src/types/types.tsx index d6403bc..d91bc60 100644 --- a/packages/admin/src/types/types.tsx +++ b/packages/admin/src/types/types.tsx @@ -22,6 +22,7 @@ export type RegistrationForm = { export type MetaForm = { name: string; coordinates: Coordinates; + pageDurationSec?: number | string; }; export type NotificationForm = Omit; diff --git a/packages/backend/src/kiosk/kiosk.service.ts b/packages/backend/src/kiosk/kiosk.service.ts index 2991ff3..77e666b 100644 --- a/packages/backend/src/kiosk/kiosk.service.ts +++ b/packages/backend/src/kiosk/kiosk.service.ts @@ -30,25 +30,14 @@ export class KioskService { }); } - async patchKiosk(kioskId: string, { widgets, style, meta }: KioskPatchDto) { - const kiosk = await this.kioskModel.findById(kioskId); - if (typeof widgets !== 'undefined') { - kiosk.config.widgets = widgets; - } - if (typeof meta !== 'undefined') { - kiosk.config.meta = meta; - } - if (typeof style !== 'undefined') { - kiosk.config.style = style; - } - return this.kioskModel.updateOne( - { _id: kioskId }, - { - $set: { - config: kiosk.config, - }, - } - ); + async patchKiosk(kioskId: string, { widgets, style, meta, pages }: KioskPatchDto) { + const $set: Record = {}; + if (typeof widgets !== 'undefined') $set['config.widgets'] = widgets; + if (typeof pages !== 'undefined') $set['config.pages'] = pages; + if (typeof meta !== 'undefined') $set['config.meta'] = meta; + if (typeof style !== 'undefined') $set['config.style'] = style; + if (Object.keys($set).length === 0) return { acknowledged: true, matchedCount: 0, modifiedCount: 0 } as never; + return this.kioskModel.updateOne({ _id: kioskId }, { $set }); } async patchWidget(kioskId: string, widget: WidgetPatchDto) { diff --git a/packages/backend/src/types/kiosk.types.ts b/packages/backend/src/types/kiosk.types.ts index 49638a9..f492d47 100644 --- a/packages/backend/src/types/kiosk.types.ts +++ b/packages/backend/src/types/kiosk.types.ts @@ -1,7 +1,9 @@ export type KioskConfig = { style: Style; meta: Meta; - widgets: WidgetConfig[]; + // Backward compatibility: single-page widgets + widgets?: WidgetConfig[]; + pages?: Page[]; }; export type KioskNotification = { @@ -31,6 +33,7 @@ export type Coordinates = { export type Meta = { coordinates: Coordinates; name: string; + pageDurationSec?: number; }; export type Style = { @@ -53,6 +56,13 @@ export type ColorModeColor = { dark: string; }; +export type Page = { + // Duration for this page in seconds + durationSec?: number; + title?: string; + widgets: WidgetConfig[]; +}; + export type WidgetName = | 'weather' | 'schpincer' diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 44cfbe3..fd3663e 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -8,15 +8,16 @@ import { useColorsOfScheme } from './utils/useColorsOfScheme'; function App() { const { background } = useColorsOfScheme(); - const { - config: { widgets }, - } = useConfig(); + const { widgets } = useConfig(); return (
{widgets.map((w) => ( - + ))}