Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
141 changes: 136 additions & 5 deletions packages/admin/src/pages/Widgets.page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,154 @@
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 value = e.target.value;
if (value === '') {
setSelected(0);
} else {
const v = parseInt(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) => {
const v = Number(e.target.value);
setDuration(Number.isNaN(v) ? 10 : Math.max(1, v));
}}
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 };
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