Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/admin/src/components/KioskListItem.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<Stat
onClick={() => setSelectedKiosk(id)}
onClick={() => {
setSelectedKiosk(id);
navigate('/dashboard');
}}
borderColor={color}
cursor='pointer'
_hover={{ backgroundColor: color }}
Expand Down
1 change: 0 additions & 1 deletion packages/admin/src/context/kiosk.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export function KioskProvider() {
.get<Kiosk>(`${ApiPaths.KIOSK}/${kioskId}`)
.then((res) => {
setKiosk(res.data);
navigate(UIPaths.DASHBOARD);
})
.catch((err) => {
if (isAxiosError(err) && err.response?.status === 401) {
Expand Down
16 changes: 15 additions & 1 deletion packages/admin/src/pages/KioskDashboard.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,21 @@ export function KioskDashboardPage() {
</PageSection>
<PageSection>
<Heading size='md'>{l('page.dashboard.enabledWidgets')}</Heading>
{kiosk?.config.widgets.map((w) => <Text key={w.name}>{WidgetDisplay[w.name].name}</Text>)}
{kiosk?.config.pages && kiosk.config.pages.length > 0 ? (
<>
<Text>Oldalak: {kiosk.config.pages.length}</Text>
{kiosk.config.pages.map((p, idx) => (
<Text
key={`page-${p.title || p.widgets?.[0]?.name || 'x'}-${p.durationSec || 10}-${p.widgets.length}`}
>
#{idx + 1}
{p.title ? ` • ${p.title}` : ''} • {p.widgets.length} csempe • {p.durationSec || 10}s
</Text>
))}
</>
) : (
kiosk?.config.widgets?.map((w) => <Text key={w.name}>{WidgetDisplay[w.name].name}</Text>)
)}
</PageSection>
{role && role.role >= 2 && (
<PageSection>
Expand Down
9 changes: 9 additions & 0 deletions packages/admin/src/pages/Meta.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -73,6 +78,10 @@ export function MetaPage() {
<Input {...register('name')} />
{Boolean(errors.name) && <FormErrorMessage>{errors.name?.message}</FormErrorMessage>}
</FormControl>
<FormControl>
<FormLabel>Oldal váltás alapértelmezett ideje (mp)</FormLabel>
<Input type='number' min={1} {...register('pageDurationSec')} />
</FormControl>
</VStack>
{isError && <FormErrorMessage>{l('error.save')}</FormErrorMessage>}
</CardBody>
Expand Down
2 changes: 1 addition & 1 deletion packages/admin/src/pages/WidgetEditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function WidgetEditPage() {
<Page title={l('title.widgetEdit')}>
<CardBody>
<Wrap gap={3} w='fit-content'>
{kiosk?.config.widgets.map((widget) => <WidgetListItem widget={widget} key={widget.name} />)}
{kiosk?.config.widgets?.map((widget) => <WidgetListItem widget={widget} key={widget.name} />)}
</Wrap>
</CardBody>
</Page>
Expand Down
133 changes: 128 additions & 5 deletions packages/admin/src/pages/Widgets.page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Page title={l('title.widgets')} isLoading={isLoading}>
<CardBody>
<WidgetGrid widgets={widgets || []} onChange={setWidgets} />
<HStack justifyContent='space-between' mb={3} flexWrap='wrap'>
<HStack>
<Button size='sm' leftIcon={<TbCirclePlus />} onClick={addPage}>
{l('button.add')} oldal
</Button>
<Button
size='sm'
colorScheme='red'
leftIcon={<TbTrash />}
onClick={removePage}
isDisabled={pages.length <= 1}
>
Oldal törlése
</Button>
<Button size='sm' leftIcon={<TbCopy />} onClick={duplicatePage} isDisabled={pages.length < 1}>
Oldal duplikálása
</Button>
</HStack>
<HStack>
<Button
size='sm'
variant='outline'
onClick={prevPage}
leftIcon={<TbChevronLeft />}
isDisabled={selected === 0}
>
Előző
</Button>
<Text>Oldal:</Text>
<Input
type='number'
min={1}
max={pages.length}
value={selected + 1}
onChange={(e) => {
const v = parseInt(e.target.value, 10);
if (!Number.isNaN(v)) setSelected(Math.min(Math.max(0, v - 1), pages.length - 1));
}}
width='4rem'
/>
<Button
size='sm'
variant='outline'
onClick={nextPage}
rightIcon={<TbChevronRight />}
isDisabled={selected >= pages.length - 1}
>
Következő
</Button>
<Text>Időtartam (s):</Text>
<Input
type='number'
min={1}
value={pages[selected]?.durationSec || 10}
onChange={(e) => setDuration(Math.max(1, parseInt(e.target.value || '10', 10)))}
width='6rem'
/>
<Text>Cím:</Text>
<Input
placeholder='Oldal címe'
value={pages[selected]?.title || ''}
onChange={(e) => setTitle(e.target.value)}
width='12rem'
/>
</HStack>
</HStack>
<WidgetGrid widgets={pages[selected]?.widgets || []} onChange={setWidgets} />
{isError && <FormErrorMessage>{l('error.save')}</FormErrorMessage>}
</CardBody>
<CardFooter>
Expand Down
12 changes: 11 additions & 1 deletion packages/admin/src/types/kiosk.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -47,6 +49,7 @@ export type Coordinates = {
export type Meta = {
coordinates: Coordinates;
name: string;
pageDurationSec?: number;
};

export type Style = {
Expand All @@ -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'
Expand Down
1 change: 1 addition & 0 deletions packages/admin/src/types/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type RegistrationForm = {
export type MetaForm = {
name: string;
coordinates: Coordinates;
pageDurationSec?: number | string;
};

export type NotificationForm = Omit<KioskNotification, 'status'>;
Expand Down
27 changes: 8 additions & 19 deletions packages/backend/src/kiosk/kiosk.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = {};
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) {
Expand Down
12 changes: 11 additions & 1 deletion packages/backend/src/types/kiosk.types.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -31,6 +33,7 @@ export type Coordinates = {
export type Meta = {
coordinates: Coordinates;
name: string;
pageDurationSec?: number;
};

export type Style = {
Expand All @@ -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'
Expand Down
9 changes: 5 additions & 4 deletions packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import { useColorsOfScheme } from './utils/useColorsOfScheme';

function App() {
const { background } = useColorsOfScheme();
const {
config: { widgets },
} = useConfig();
const { widgets } = useConfig();
return (
<Main backgroundColor={background}>
<Titlebar />
<WidgetGrid>
{widgets.map((w) => (
<WidgetDistributor key={w.name} config={w} />
<WidgetDistributor
key={`${w.name}-${w.grid.row.start}-${w.grid.row.end}-${w.grid.column.start}-${w.grid.column.end}`}
config={w}
/>
))}
</WidgetGrid>
<Footer />
Expand Down
13 changes: 13 additions & 0 deletions packages/client/src/layout/ActiveWidgetConfigContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React, { createContext, PropsWithChildren, useContext } from 'react';

import { WidgetConfig, WidgetConfigBase } from '@/types/widget.type';

const ActiveWidgetConfigContext = createContext<WidgetConfig | undefined>(undefined);

export const ActiveWidgetProvider: React.FC<PropsWithChildren<{ config: WidgetConfig }>> = ({ config, children }) => {
return <ActiveWidgetConfigContext.Provider value={config}>{children}</ActiveWidgetConfigContext.Provider>;
};

export function useActiveWidgetConfig<T extends WidgetConfigBase>(): T | undefined {
return useContext(ActiveWidgetConfigContext) as T | undefined;
}
Loading