From e7ab19c62263c331fe68e8714a21a63289e96037 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sun, 1 Oct 2023 00:11:05 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Add=20working=20loading=20of=20b?= =?UTF-8?q?oard,=20add=20temporary=20input=20to=20manage=20page=20to=20add?= =?UTF-8?q?=20boards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 22 +- src/components/Board/context.tsx | 65 ++++++ src/components/Dashboard/Dashboard.tsx | 5 +- .../Dashboard/Tiles/Apps/AppPing.tsx | 30 ++- .../Dashboard/Tiles/Apps/AppTile.tsx | 40 ++-- .../Dashboard/Tiles/Widgets/WidgetsMenu.tsx | 11 +- .../Dashboard/Views/DashboardView.tsx | 43 ++-- src/components/Dashboard/Views/DetailView.tsx | 4 +- src/components/Dashboard/Views/EditView.tsx | 4 +- .../Dashboard/Wrappers/Category/Category.tsx | 49 ++-- .../Dashboard/Wrappers/Wrapper/Wrapper.tsx | 17 +- .../Dashboard/Wrappers/WrapperContent.tsx | 35 +-- .../Wrappers/gridstack/init-gridstack.ts | 51 ++-- .../Dashboard/Wrappers/gridstack/store.tsx | 6 +- .../Wrappers/gridstack/use-gridstack.ts | 64 ++--- src/components/layout/Common/Logo.tsx | 29 ++- src/components/layout/Common/useCardStyles.ts | 9 +- src/components/layout/Common/useGradient.tsx | 1 - .../layout/Meta/BoardHeadOverride.tsx | 18 +- .../layout/Templates/BoardLayout.tsx | 24 +- src/components/layout/header/Search.tsx | 47 ++-- src/pages/_app.tsx | 15 +- src/pages/board/[slug].tsx | 4 +- src/pages/board/index.tsx | 70 ++---- src/pages/manage/index.tsx | 39 +++- src/server/api/routers/board.ts | 218 +++++++++++++++++- src/server/db/schema.ts | 16 +- src/tools/server/loginBuilder.ts | 7 +- src/widgets/WidgetWrapper.tsx | 17 +- src/widgets/date/DateTile.tsx | 34 +-- src/widgets/notebook/NotebookEditor.tsx | 16 +- src/widgets/notebook/NotebookWidgetTile.tsx | 5 +- src/widgets/weather/WeatherTile.tsx | 27 +-- src/widgets/widgets.ts | 10 +- yarn.lock | 40 ++-- 35 files changed, 674 insertions(+), 418 deletions(-) create mode 100644 src/components/Board/context.tsx diff --git a/package.json b/package.json index fd080071e..ffa5066e6 100644 --- a/package.json +++ b/package.json @@ -35,16 +35,16 @@ "@emotion/react": "^11.10.6", "@emotion/server": "^11.10.0", "@jellyfin/sdk": "^0.8.0", - "@mantine/core": "^6.0.0", - "@mantine/dates": "^6.0.0", - "@mantine/dropzone": "^6.0.0", - "@mantine/form": "^6.0.0", - "@mantine/hooks": "^6.0.0", - "@mantine/modals": "^6.0.0", - "@mantine/next": "^6.0.0", - "@mantine/notifications": "^6.0.0", - "@mantine/prism": "^6.0.19", - "@mantine/tiptap": "^6.0.17", + "@mantine/core": "^6.0.21", + "@mantine/dates": "^6.0.21", + "@mantine/dropzone": "^6.0.21", + "@mantine/form": "^6.0.21", + "@mantine/hooks": "^6.0.21", + "@mantine/modals": "^6.0.21", + "@mantine/next": "^6.0.21", + "@mantine/notifications": "^6.0.21", + "@mantine/prism": "^6.0.21", + "@mantine/tiptap": "^6.0.21", "@nivo/core": "^0.83.0", "@nivo/line": "^0.83.0", "@react-native-async-storage/async-storage": "^1.18.1", @@ -250,4 +250,4 @@ ] } } -} \ No newline at end of file +} diff --git a/src/components/Board/context.tsx b/src/components/Board/context.tsx new file mode 100644 index 000000000..9204baeb5 --- /dev/null +++ b/src/components/Board/context.tsx @@ -0,0 +1,65 @@ +import { createContext, useContext } from 'react'; +import { RouterOutputs, api } from '~/utils/api'; + +type BoardContextType = { + layout?: string; + board: RouterOutputs['boards']['byName']; +}; +const BoardContext = createContext(null); +type BoardProviderProps = { + initialBoard: RouterOutputs['boards']['byName']; + layout?: string; + children: React.ReactNode; +}; +export const BoardProvider = ({ children, ...props }: BoardProviderProps) => { + const { data: board } = api.boards.byName.useQuery( + { + boardName: props.initialBoard.name, + layout: props.layout, + }, + { + initialData: props.initialBoard, + } + ); + + return ( + + {children} + + ); +}; + +export const useRequiredBoard = () => { + const ctx = useContext(BoardContext); + if (!ctx) throw new Error('useBoard must be used within a BoardProvider'); + return ctx.board; +}; + +export const useOptionalBoard = () => { + const ctx = useContext(BoardContext); + return ctx?.board ?? null; +}; + +export type Section = RouterOutputs['boards']['byName']['sections'][number]; +type SectionOfType< + TSection extends Section, + TSectionType extends Section['type'], +> = TSection extends { type: TSectionType } ? TSection : never; +export type CategorySection = SectionOfType; +export type EmptySection = SectionOfType; +export type SidebarSection = SectionOfType; +export type HiddenSection = SectionOfType; + +export type Item = Section['items'][number]; +type ItemOfType = TItem extends { + type: TItemType; +} + ? TItem + : never; +export type AppItem = ItemOfType; +export type WidgetItem = ItemOfType; diff --git a/src/components/Dashboard/Dashboard.tsx b/src/components/Dashboard/Dashboard.tsx index 35acd4d39..0802b79d4 100644 --- a/src/components/Dashboard/Dashboard.tsx +++ b/src/components/Dashboard/Dashboard.tsx @@ -1,15 +1,16 @@ import { MobileRibbons } from './Mobile/Ribbon/MobileRibbon'; +import { BoardView } from './Views/DashboardView'; import { DashboardDetailView } from './Views/DetailView'; import { DashboardEditView } from './Views/EditView'; import { useEditModeStore } from './Views/useEditModeStore'; -export const Dashboard = () => { +export const Board = () => { const isEditMode = useEditModeStore((x) => x.enabled); return ( <> {/* The following elemens are splitted because gridstack doesn't reinitialize them when using same item. */} - {isEditMode ? : } + ); diff --git a/src/components/Dashboard/Tiles/Apps/AppPing.tsx b/src/components/Dashboard/Tiles/Apps/AppPing.tsx index bf5e89f03..a75bc7db9 100644 --- a/src/components/Dashboard/Tiles/Apps/AppPing.tsx +++ b/src/components/Dashboard/Tiles/Apps/AppPing.tsx @@ -4,19 +4,19 @@ import Consola from 'consola'; import { TargetAndTransition, Transition, motion } from 'framer-motion'; import { useSession } from 'next-auth/react'; import { useTranslation } from 'next-i18next'; -import { RouterOutputs, api } from '~/utils/api'; - +import { AppItem } from '~/components/Board/context'; import { useConfigContext } from '~/config/provider'; import { AppType } from '~/types/app'; +import { RouterOutputs, api } from '~/utils/api'; interface AppPingProps { - app: AppType; + app: AppItem; } export const AppPing = ({ app }: AppPingProps) => { const { data: sessionData } = useSession(); const { data: userWithSettings } = api.user.withSettings.useQuery(undefined, { - enabled: app.network.enabledStatusChecker && !!sessionData?.user, + enabled: app.isPingEnabled && !!sessionData?.user, }); const { data, isFetching, isError, error, isActive } = usePing(app); @@ -74,13 +74,6 @@ const AccessibleIndicatorPing = ({ isFetching, isOnline }: AccessibleIndicatorPi return ; }; -export const isStatusOk = (app: AppType, status: number) => { - if (app.network.okStatus === undefined || app.network.statusCodes.length >= 1) { - return app.network.statusCodes.includes(status.toString()); - } - return app.network.okStatus.includes(status); -}; - type TooltipLabelProps = { isFetching: boolean; isError: boolean; @@ -97,11 +90,10 @@ const useTooltipLabel = ({ isFetching, isError, data, errorMessage }: TooltipLab return `${data?.statusText}: ${data?.status} (denied)`; }; -const usePing = (app: AppType) => { +const usePing = (app: AppItem) => { const { config, name } = useConfigContext(); const isActive = - (config?.settings.customization.layout.enabledPing && app.network.enabledStatusChecker) ?? - false; + (config?.settings.customization.layout.enabledPing && app.isPingEnabled) ?? false; const queryResult = api.app.ping.useQuery( { @@ -122,11 +114,15 @@ const usePing = (app: AppType) => { retryOnMount: true, select: (data) => { - const isOk = isStatusOk(app, data.status); + const isOk = app.statusCodes.includes(data.status); if (isOk) - Consola.info(`Ping of app "${app.name}" (${app.url}) returned ${data.status} (Accepted)`); + Consola.info( + `Ping of app "${app.name}" (${app.internalUrl}) returned ${data.status} (Accepted)` + ); else - Consola.warn(`Ping of app "${app.name}" (${app.url}) returned ${data.status} (Refused)`); + Consola.warn( + `Ping of app "${app.name}" (${app.internalUrl}) returned ${data.status} (Refused)` + ); return { status: data.status, state: isOk ? ('online' as const) : ('down' as const), diff --git a/src/components/Dashboard/Tiles/Apps/AppTile.tsx b/src/components/Dashboard/Tiles/Apps/AppTile.tsx index b3c60a1ff..7253aee61 100644 --- a/src/components/Dashboard/Tiles/Apps/AppTile.tsx +++ b/src/components/Dashboard/Tiles/Apps/AppTile.tsx @@ -1,9 +1,9 @@ -import { Affix, Box, Text, Tooltip, UnstyledButton } from '@mantine/core'; +import { Box, Text, Tooltip, UnstyledButton } from '@mantine/core'; import { createStyles, useMantineTheme } from '@mantine/styles'; import { motion } from 'framer-motion'; import Link from 'next/link'; +import { AppItem } from '~/components/Board/context'; -import { AppType } from '~/types/app'; import { useEditModeStore } from '../../Views/useEditModeStore'; import { HomarrCardWrapper } from '../HomarrCardWrapper'; import { BaseTileProps } from '../type'; @@ -11,21 +11,25 @@ import { AppMenu } from './AppMenu'; import { AppPing } from './AppPing'; interface AppTileProps extends BaseTileProps { - app: AppType; + app: AppItem; } +const namePositions = { + right: 'row', + left: 'row-reverse', + top: 'column', + bottom: 'column-reverse', +}; + export const AppTile = ({ className, app }: AppTileProps) => { const isEditMode = useEditModeStore((x) => x.enabled); const { cx, classes } = useStyles(); const { colorScheme } = useMantineTheme(); - const tooltipContent = [ - app.appearance.appNameStatus === 'hover' ? app.name : undefined, - app.behaviour.tooltipDescription, - ] + const tooltipContent = [app.nameStyle === 'hover' ? app.name : undefined, app.description] .filter((e) => e) .join(': '); - const isRow = app.appearance.positionAppName.includes('row'); + const isRow = app.namePosition === 'right' || app.namePosition === 'left'; function Inner() { return ( @@ -42,26 +46,26 @@ export const AppTile = ({ className, app }: AppTileProps) => { className={`${classes.base} ${cx(classes.appContent, 'dashboard-tile-app')}`} h="100%" sx={{ - flexFlow: app.appearance.positionAppName ?? 'column', + flexFlow: namePositions[app.namePosition] ?? 'column', }} > - {app.appearance.appNameStatus === 'normal' && ( + {app.nameStyle === 'show' && ( {app.name} )} { ); } + const url = app.externalUrl ? app.externalUrl : app.internalUrl; + return ( - {!app.url || isEditMode ? ( + {!url || isEditMode ? ( { 0 ? app.behaviour.externalUrl : app.url} - target={app.behaviour.isOpeningNewTab ? '_blank' : '_self'} + href={url} + target={app.openInNewTab ? '_blank' : '_self'} className={`${classes.button} ${classes.base}`} > @@ -111,7 +117,7 @@ const useStyles = createStyles((theme, _params, getRef) => ({ overflow: 'visible', flexGrow: 5, }, - appImage:{ + appImage: { maxHeight: '100%', maxWidth: '100%', overflow: 'auto', diff --git a/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx b/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx index fecb60641..abc52c417 100644 --- a/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx +++ b/src/components/Dashboard/Tiles/Widgets/WidgetsMenu.tsx @@ -1,9 +1,10 @@ import { Title } from '@mantine/core'; import { useTranslation } from 'next-i18next'; - +import { WidgetItem } from '~/components/Board/context'; import { openContextModalGeneric } from '~/tools/mantineModalManagerExtensions'; -import WidgetsDefinitions from '../../../../widgets'; import { IWidget } from '~/widgets/widgets'; + +import WidgetsDefinitions from '../../../../widgets'; import { useWrapperColumnCount } from '../../Wrappers/gridstack/store'; import { GenericTileMenu } from '../GenericTileMenu'; import { WidgetEditModalInnerProps } from './WidgetsEditModal'; @@ -18,7 +19,7 @@ export type WidgetChangePositionModalInnerProps = { interface WidgetsMenuProps { integration: string; - widget: IWidget | undefined; + widget: WidgetItem | undefined; } export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => { @@ -67,7 +68,7 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => { innerProps: { widgetId: widget.id, widgetType: integration, - options: widget.properties, + options: widget.options, // Cast as the right type for the correct widget widgetOptions: widgetDefinitionObject.options as any, }, @@ -81,7 +82,7 @@ export const WidgetsMenu = ({ integration, widget }: WidgetsMenuProps) => { handleClickChangePosition={handleChangeSizeClick} handleClickDelete={handleDeleteClick} displayEdit={ - typeof widget.properties !== 'undefined' && + typeof widget.options !== 'undefined' && Object.keys(widgetDefinitionObject?.options ?? {}).length !== 0 } /> diff --git a/src/components/Dashboard/Views/DashboardView.tsx b/src/components/Dashboard/Views/DashboardView.tsx index 3c75e3dbd..a59a8c3b9 100644 --- a/src/components/Dashboard/Views/DashboardView.tsx +++ b/src/components/Dashboard/Views/DashboardView.tsx @@ -1,18 +1,15 @@ import { Group, Stack } from '@mantine/core'; -import { useEffect, useMemo, useRef } from 'react'; - -import { useConfigContext } from '~/config/provider'; +import { useEffect, useRef } from 'react'; +import { CategorySection, EmptySection, useRequiredBoard } from '~/components/Board/context'; import { useResize } from '~/hooks/use-resize'; import { useScreenLargerThan } from '~/hooks/useScreenLargerThan'; -import { CategoryType } from '~/types/category'; -import { WrapperType } from '~/types/wrapper'; -import { DashboardCategory } from '../Wrappers/Category/Category'; + import { DashboardSidebar } from '../Wrappers/Sidebar/Sidebar'; import { DashboardWrapper } from '../Wrappers/Wrapper/Wrapper'; import { useGridstackStore } from '../Wrappers/gridstack/store'; -export const DashboardView = () => { - const wrappers = useWrapperItems(); +export const BoardView = () => { + const sections = useStackedSections(); const sidebarsVisible = useSidebarVisibility(); const { isReady, mainAreaRef } = usePrepareGridstack(); @@ -24,11 +21,11 @@ export const DashboardView = () => { {isReady && - wrappers.map((item) => + sections.map((item) => item.type === 'category' ? ( - + {item.name} ) : ( - + ) )} @@ -39,6 +36,8 @@ export const DashboardView = () => { ); }; +// +// const usePrepareGridstack = () => { const mainAreaRef = useRef(null); @@ -58,29 +57,21 @@ const usePrepareGridstack = () => { }; const useSidebarVisibility = () => { - const layoutSettings = useConfigContext()?.config?.settings.customization.layout; + const board = useRequiredBoard(); const screenLargerThanMd = useScreenLargerThan('md'); // For smaller screens mobile ribbons are displayed with drawers const isScreenSizeUnknown = typeof screenLargerThanMd === 'undefined'; return { - right: layoutSettings?.enabledRightSidebar && screenLargerThanMd, - left: layoutSettings?.enabledLeftSidebar && screenLargerThanMd, + right: board.isRightSidebarVisible && screenLargerThanMd, + left: board.isLeftSidebarVisible && screenLargerThanMd, isLoading: isScreenSizeUnknown, }; }; -const useWrapperItems = () => { - const { config } = useConfigContext(); - - return useMemo( - () => - config - ? [ - ...config.categories.map((c) => ({ ...c, type: 'category' })), - ...config.wrappers.map((w) => ({ ...w, type: 'wrapper' })), - ].sort((a, b) => a.position - b.position) - : [], - [config?.categories, config?.wrappers] +const useStackedSections = () => { + const board = useRequiredBoard(); + return board.sections.filter( + (s): s is CategorySection | EmptySection => s.type === 'category' || s.type === 'empty' ); }; diff --git a/src/components/Dashboard/Views/DetailView.tsx b/src/components/Dashboard/Views/DetailView.tsx index e73c664bc..76cde8055 100644 --- a/src/components/Dashboard/Views/DetailView.tsx +++ b/src/components/Dashboard/Views/DetailView.tsx @@ -1,3 +1,3 @@ -import { DashboardView } from './DashboardView'; +import { BoardView } from './DashboardView'; -export const DashboardDetailView = () => ; +export const DashboardDetailView = () => ; diff --git a/src/components/Dashboard/Views/EditView.tsx b/src/components/Dashboard/Views/EditView.tsx index 11b387d81..11a4b180d 100644 --- a/src/components/Dashboard/Views/EditView.tsx +++ b/src/components/Dashboard/Views/EditView.tsx @@ -1,3 +1,3 @@ -import { DashboardView } from './DashboardView'; +import { BoardView } from './DashboardView'; -export const DashboardEditView = () => ; +export const DashboardEditView = () => ; diff --git a/src/components/Dashboard/Wrappers/Category/Category.tsx b/src/components/Dashboard/Wrappers/Category/Category.tsx index 2e9acc908..7ed18d061 100644 --- a/src/components/Dashboard/Wrappers/Category/Category.tsx +++ b/src/components/Dashboard/Wrappers/Category/Category.tsx @@ -9,13 +9,11 @@ import { Title, createStyles, } from '@mantine/core'; -import { useLocalStorage } from '@mantine/hooks'; import { modals } from '@mantine/modals'; import { IconDotsVertical, IconShare3 } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; +import { CategorySection } from '~/components/Board/context'; -import { useConfigContext } from '~/config/provider'; -import { CategoryType } from '~/types/category'; import { useCardStyles } from '../../../layout/Common/useCardStyles'; import { useEditModeStore } from '../../Views/useEditModeStore'; import { WrapperContent } from '../WrapperContent'; @@ -23,25 +21,24 @@ import { useGridstack } from '../gridstack/use-gridstack'; import { CategoryEditMenu } from './CategoryEditMenu'; interface DashboardCategoryProps { - category: CategoryType; + section: CategorySection; } -export const DashboardCategory = ({ category }: DashboardCategoryProps) => { - const { refs, apps, widgets } = useGridstack('category', category.id); +export const DashboardCategory = ({ section }: DashboardCategoryProps) => { + const { refs } = useGridstack({ section }); const isEditMode = useEditModeStore((x) => x.enabled); - const { config } = useConfigContext(); const { classes: cardClasses, cx } = useCardStyles(true); const { classes } = useStyles(); const { t } = useTranslation(['layout/common', 'common']); - const categoryList = config?.categories.map((x) => x.name) ?? []; - const [toggledCategories, setToggledCategories] = useLocalStorage({ + //const categoryList = config?.categories.map((x) => x.name) ?? []; + /*const [toggledCategories, setToggledCategories] = useLocalStorage({ key: `${config?.configProperties.name}-app-shelf-toggled`, // This is a bit of a hack to toggle the categories on the first load, return a string[] of the categories defaultValue: categoryList, - }); + });*/ - const handleMenuClick = () => { + /*const handleMenuClick = () => { for (let i = 0; i < apps.length; i += 1) { const app = apps[i]; const popUp = window.open(app.url, app.id); @@ -79,6 +76,15 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => { } }; + // value={isEditMode ? categoryList : toggledCategories} + /* + onChange={(state) => { + // Cancel if edit mode is on + if (isEditMode) return; + setToggledCategories([...state]); + }} + */ + return ( { mx={10} chevronPosition="left" multiple - value={isEditMode ? categoryList : toggledCategories} variant="separated" radius="lg" - onChange={(state) => { - // Cancel if edit mode is on - if (isEditMode) return; - setToggledCategories([...state]); - }} > - + - }> - {category.name} + }> + {section.name} {!isEditMode && ( @@ -109,7 +109,10 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => { - }> + } + > {t('actions.category.openAllInNewTab')} @@ -119,10 +122,10 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
- +
diff --git a/src/components/Dashboard/Wrappers/Wrapper/Wrapper.tsx b/src/components/Dashboard/Wrappers/Wrapper/Wrapper.tsx index 71f88dd5b..78498ec3a 100644 --- a/src/components/Dashboard/Wrappers/Wrapper/Wrapper.tsx +++ b/src/components/Dashboard/Wrappers/Wrapper/Wrapper.tsx @@ -1,29 +1,30 @@ -import { WrapperType } from '~/types/wrapper'; +import { EmptySection } from '~/components/Board/context'; + import { useEditModeStore } from '../../Views/useEditModeStore'; import { WrapperContent } from '../WrapperContent'; import { useGridstack } from '../gridstack/use-gridstack'; interface DashboardWrapperProps { - wrapper: WrapperType; + section: EmptySection; } -export const DashboardWrapper = ({ wrapper }: DashboardWrapperProps) => { - const { refs, apps, widgets } = useGridstack('wrapper', wrapper.id); +export const DashboardWrapper = ({ section }: DashboardWrapperProps) => { + const { refs } = useGridstack({ section }); const isEditMode = useEditModeStore((x) => x.enabled); - const defaultClasses = 'grid-stack grid-stack-wrapper min-row'; + const defaultClasses = 'grid-stack grid-stack-empty min-row'; return (
0 || widgets.length > 0 || isEditMode + section.items.length > 0 || isEditMode ? defaultClasses : `${defaultClasses} gridstack-empty-wrapper` } style={{ transitionDuration: '0s' }} - data-wrapper={wrapper.id} + data-empty={section.id} ref={refs.wrapper} > - +
); }; diff --git a/src/components/Dashboard/Wrappers/WrapperContent.tsx b/src/components/Dashboard/Wrappers/WrapperContent.tsx index 5fc531ac5..733e847c0 100644 --- a/src/components/Dashboard/Wrappers/WrapperContent.tsx +++ b/src/components/Dashboard/Wrappers/WrapperContent.tsx @@ -1,17 +1,17 @@ import { GridStack } from 'fily-publish-gridstack'; -import { MutableRefObject, RefObject } from 'react'; - +import { MutableRefObject, RefObject, useMemo } from 'react'; +import { AppItem, Item, WidgetItem } from '~/components/Board/context'; import { AppType } from '~/types/app'; -import Widgets from '../../../widgets'; import { WidgetWrapper } from '~/widgets/WidgetWrapper'; import { IWidget, IWidgetDefinition } from '~/widgets/widgets'; + +import Widgets from '../../../widgets'; import { appTileDefinition } from '../Tiles/Apps/AppTile'; import { GridstackTileWrapper } from '../Tiles/TileWrapper'; import { useGridstackStore } from './gridstack/store'; interface WrapperContentProps { - apps: AppType[]; - widgets: IWidget[]; + items: Item[]; refs: { wrapper: RefObject; items: MutableRefObject>>; @@ -19,10 +19,9 @@ interface WrapperContentProps { }; } -export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) { - const shapeSize = useGridstackStore((x) => x.currentShapeSize); - - if (!shapeSize) return null; +export function WrapperContent({ items, refs }: WrapperContentProps) { + const apps = useMemo(() => items.filter((x): x is AppItem => x.type === 'app'), [items]); + const widgets = useMemo(() => items.filter((x): x is WidgetItem => x.type === 'widget'), [items]); return ( <> @@ -35,17 +34,17 @@ export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) { key={app.id} itemRef={refs.items.current[app.id]} {...tile} - {...(app.shape[shapeSize]?.location ?? {})} - {...(app.shape[shapeSize]?.size ?? {})} + x={app.x} + y={app.y} + width={app.width} + height={app.height} > ); })} {widgets.map((widget) => { - const definition = Widgets[widget.type as keyof typeof Widgets] as - | IWidgetDefinition - | undefined; + const definition = Widgets[widget.sort]; if (!definition) return null; return ( @@ -55,14 +54,16 @@ export function WrapperContent({ apps, refs, widgets }: WrapperContentProps) { itemRef={refs.items.current[widget.id]} id={widget.id} {...definition.gridstack} - {...widget.shape[shapeSize]?.location} - {...widget.shape[shapeSize]?.size} + x={widget.x} + y={widget.y} + width={widget.width} + height={widget.height} > ); diff --git a/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts b/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts index 763b9da05..25ea258e1 100644 --- a/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts +++ b/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts @@ -1,9 +1,6 @@ -import { GridItemHTMLElement, GridStack, GridStackNode } from 'fily-publish-gridstack'; +import { GridStack, GridStackNode } from 'fily-publish-gridstack'; import { MutableRefObject, RefObject } from 'react'; - -import { AppType } from '~/types/app'; -import { ShapeType } from '~/types/shape'; -import { IWidget } from '~/widgets/widgets'; +import { Item } from '~/components/Board/context'; export const initializeGridstack = ( areaType: 'wrapper' | 'category' | 'sidebar', @@ -11,8 +8,7 @@ export const initializeGridstack = ( gridRef: MutableRefObject, itemRefs: MutableRefObject>>, areaId: string, - items: AppType[], - widgets: IWidget[], + items: Item[], isEditMode: boolean, wrapperColumnCount: number, shapeSize: 'sm' | 'md' | 'lg', @@ -45,6 +41,7 @@ export const initializeGridstack = ( `.grid-stack-${areaType}[data-${areaType}='${areaId}']` ); const grid = newGrid.current; + if (!grid) return; // Must be used to update the column count after the initialization grid.column(columnCount, 'none'); @@ -66,39 +63,27 @@ export const initializeGridstack = ( grid.batchUpdate(); grid.removeAll(false); - items.forEach(({ id, shape }) => { - const item = itemRefs.current[id]?.current; - setAttributesFromShape(item, shape[shapeSize]); - item && grid.makeWidget(item as HTMLDivElement); - if (!shape[shapeSize] && item) { - const gridItemElement = item as GridItemHTMLElement; + items.forEach((item) => { + const ref = itemRefs.current[item.id]?.current; + setAttributesFromShape(ref, item); + ref && grid.makeWidget(ref as HTMLDivElement); + /*if (!item && ref) { + const gridItemElement = ref as GridItemHTMLElement; if (gridItemElement.gridstackNode) { const { x, y, w, h } = gridItemElement.gridstackNode; - tilesWithUnknownLocation.push({ x, y, w, h, type: 'app', id }); + tilesWithUnknownLocation.push({ x, y, w, h, type: 'app', id: item.id }); } - } - }); - widgets.forEach(({ id, shape }) => { - const item = itemRefs.current[id]?.current; - setAttributesFromShape(item, shape[shapeSize]); - item && grid.makeWidget(item as HTMLDivElement); - if (!shape[shapeSize] && item) { - const gridItemElement = item as GridItemHTMLElement; - if (gridItemElement.gridstackNode) { - const { x, y, w, h } = gridItemElement.gridstackNode; - tilesWithUnknownLocation.push({ x, y, w, h, type: 'widget', id }); - } - } + }*/ }); grid.batchUpdate(false); }; -function setAttributesFromShape(ref: HTMLDivElement | null, sizedShape: ShapeType['lg']) { - if (!sizedShape || !ref) return; - ref.setAttribute('gs-x', sizedShape.location.x.toString()); - ref.setAttribute('gs-y', sizedShape.location.y.toString()); - ref.setAttribute('gs-w', sizedShape.size.width.toString()); - ref.setAttribute('gs-h', sizedShape.size.height.toString()); +function setAttributesFromShape(ref: HTMLDivElement | null, item: Item) { + if (!item || !ref) return; + ref.setAttribute('gs-x', item.x.toString()); + ref.setAttribute('gs-y', item.y.toString()); + ref.setAttribute('gs-w', item.width.toString()); + ref.setAttribute('gs-h', item.height.toString()); } export type TileWithUnknownLocation = { diff --git a/src/components/Dashboard/Wrappers/gridstack/store.tsx b/src/components/Dashboard/Wrappers/gridstack/store.tsx index f02bc16b4..ff101ecef 100644 --- a/src/components/Dashboard/Wrappers/gridstack/store.tsx +++ b/src/components/Dashboard/Wrappers/gridstack/store.tsx @@ -1,5 +1,4 @@ import { createWithEqualityFn } from 'zustand/traditional'; - import { useConfigContext } from '~/config/provider'; import { GridstackBreakpoints } from '~/constants/gridstack-breakpoints'; @@ -31,7 +30,8 @@ export const useNamedWrapperColumnCount = (): 'small' | 'medium' | 'large' | nul }; export const useWrapperColumnCount = () => { - const { config } = useConfigContext(); + return 10; + /*const { config } = useConfigContext(); if (!config) { return null; @@ -46,7 +46,7 @@ export const useWrapperColumnCount = () => { return config.settings.customization.gridstack?.columnCountSmall ?? 3; default: return null; - } + }*/ }; function getCurrentShapeSize(size: number) { diff --git a/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts b/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts index 994a5fbc8..bf0fbb4d8 100644 --- a/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts +++ b/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts @@ -1,18 +1,17 @@ import { GridStack, GridStackNode } from 'fily-publish-gridstack'; import { MutableRefObject, RefObject, createRef, useEffect, useMemo, useRef } from 'react'; - +import { Item, Section } from '~/components/Board/context'; import { useConfigContext } from '~/config/provider'; import { useConfigStore } from '~/config/store'; import { AppType } from '~/types/app'; import { AreaType } from '~/types/area'; import { IWidget } from '~/widgets/widgets'; + import { useEditModeStore } from '../../Views/useEditModeStore'; import { TileWithUnknownLocation, initializeGridstack } from './init-gridstack'; import { useGridstackStore, useWrapperColumnCount } from './store'; interface UseGristackReturnType { - apps: AppType[]; - widgets: IWidget[]; refs: { wrapper: RefObject; items: MutableRefObject>>; @@ -20,12 +19,12 @@ interface UseGristackReturnType { }; } -export const useGridstack = ( - areaType: 'wrapper' | 'category' | 'sidebar', - areaId: string -): UseGristackReturnType => { +type UseGridstackProps = { + section: Section; +}; + +export const useGridstack = ({ section }: UseGridstackProps): UseGristackReturnType => { const isEditMode = useEditModeStore((x) => x.enabled); - const { config, configVersion, name: configName } = useConfigContext(); const updateConfig = useConfigStore((x) => x.updateConfig); // define reference for wrapper - is used to calculate the width of the wrapper const wrapperRef = useRef(null); @@ -43,40 +42,17 @@ export const useGridstack = ( throw new Error('UseGridstack should not be executed before mainAreaWidth has been set!'); } - const items = useMemo( - () => - config?.apps.filter( - (x) => - x.area.type === areaType && - (x.area.type === 'sidebar' - ? x.area.properties.location === areaId - : x.area.properties.id === areaId) - ) ?? [], - [configVersion, config?.apps.length] - ); - const widgets = useMemo(() => { - if (!config) return []; - return config.widgets.filter( - (w) => - w.area.type === areaType && - (w.area.type === 'sidebar' - ? w.area.properties.location === areaId - : w.area.properties.id === areaId) - ); - }, [configVersion, config?.widgets.length]); + const items = useMemo(() => section.items, [section.items.length]); // define items in itemRefs for easy access and reference to items - if (Object.keys(itemRefs.current).length !== items.length + (widgets ?? []).length) { + if (Object.keys(itemRefs.current).length !== items.length) { items.forEach(({ id }: { id: keyof typeof itemRefs.current }) => { itemRefs.current[id] = itemRefs.current[id] || createRef(); }); - (widgets ?? []).forEach(({ id }) => { - itemRefs.current[id] = itemRefs.current[id] || createRef(); - }); } useEffect(() => { - if (areaType === 'sidebar') return; + if (section.type === 'sidebar') return; const widgetWidth = mainAreaWidth / wrapperColumnCount; // widget width is used to define sizes of gridstack items within global.scss root.style.setProperty('--gridstack-widget-width', widgetWidth.toString()); @@ -88,6 +64,7 @@ export const useGridstack = ( root.style.setProperty('--gridstack-column-count', wrapperColumnCount.toString()); }, [wrapperColumnCount]); + const configName = 'default'; const onChange = isEditMode ? (changedNode: GridStackNode) => { if (!configName) return; @@ -154,18 +131,18 @@ export const useGridstack = ( : previous.widgets.find((x) => x.id === itemId); if (!currentItem) return previous; - if (areaType === 'sidebar') { + if (section.type === 'sidebar') { currentItem.area = { - type: areaType, + type: section.type, properties: { - location: areaId as 'right' | 'left', + location: section.position, }, }; } else { currentItem.area = { - type: areaType, + type: section.type as any, properties: { - id: areaId, + id: section.id, }, }; } @@ -241,13 +218,12 @@ export const useGridstack = ( const tilesWithUnknownLocation: TileWithUnknownLocation[] = []; initializeGridstack( - areaType, + section.type as any, wrapperRef, gridRef, itemRefs, - areaId, + section.id, items, - widgets ?? [], isEditMode, wrapperColumnCount, shapeSize, @@ -308,11 +284,9 @@ export const useGridstack = ( }), })); return removeEventHandlers; - }, [items, wrapperRef.current, widgets, wrapperColumnCount]); + }, [items, wrapperRef.current, wrapperColumnCount]); return { - apps: items, - widgets: widgets ?? [], refs: { items: itemRefs, wrapper: wrapperRef, diff --git a/src/components/layout/Common/Logo.tsx b/src/components/layout/Common/Logo.tsx index 07116361e..294ec480e 100644 --- a/src/components/layout/Common/Logo.tsx +++ b/src/components/layout/Common/Logo.tsx @@ -1,7 +1,7 @@ -import { Group, Image, Text } from '@mantine/core'; +import { Group, Image, Text, useMantineTheme } from '@mantine/core'; +import { useOptionalBoard } from '~/components/Board/context'; import { useScreenLargerThan } from '~/hooks/useScreenLargerThan'; -import { useConfigContext } from '~/config/provider'; import { usePrimaryGradient } from './useGradient'; interface LogoProps { @@ -10,29 +10,42 @@ interface LogoProps { } export function Logo({ size = 'md', withoutText = false }: LogoProps) { - const { config } = useConfigContext(); + const theme = useMantineTheme(); + const board = useOptionalBoard(); const primaryGradient = usePrimaryGradient(); const largerThanMd = useScreenLargerThan('md'); + const colors = theme.fn.variant({ + variant: 'gradient', + gradient: { + from: 'red', + to: 'orange', + deg: 125, + }, + }); + return ( Homarr Logo {withoutText || !largerThanMd ? null : ( - {config?.settings.customization.pageTitle || 'Homarr'} + {board?.pageTitle || 'Homarr'} )} ); } + +// TODO: Mantine gradient didn't work +// diff --git a/src/components/layout/Common/useCardStyles.ts b/src/components/layout/Common/useCardStyles.ts index f85d0e097..4af20b74d 100644 --- a/src/components/layout/Common/useCardStyles.ts +++ b/src/components/layout/Common/useCardStyles.ts @@ -1,12 +1,11 @@ import { createStyles } from '@mantine/core'; - -import { useConfigContext } from '~/config/provider'; +import { useOptionalBoard } from '~/components/Board/context'; export const useCardStyles = (isCategory: boolean) => { - const { config } = useConfigContext(); - const appOpacity = config?.settings.customization.appOpacity; + const board = useOptionalBoard(); + const appOpacity = board?.appOpacity ?? 100; return createStyles(({ colorScheme }, _params) => { - const opacity = (appOpacity || 100) / 100; + const opacity = appOpacity / 100; if (colorScheme === 'dark') { if (isCategory) { diff --git a/src/components/layout/Common/useGradient.tsx b/src/components/layout/Common/useGradient.tsx index 4ba667f1e..566e0b626 100644 --- a/src/components/layout/Common/useGradient.tsx +++ b/src/components/layout/Common/useGradient.tsx @@ -1,5 +1,4 @@ import { MantineGradient } from '@mantine/core'; - import { useColorTheme } from '~/tools/color'; export const usePrimaryGradient = () => { diff --git a/src/components/layout/Meta/BoardHeadOverride.tsx b/src/components/layout/Meta/BoardHeadOverride.tsx index 7009489dc..7cccb7793 100644 --- a/src/components/layout/Meta/BoardHeadOverride.tsx +++ b/src/components/layout/Meta/BoardHeadOverride.tsx @@ -1,16 +1,12 @@ import Head from 'next/head'; -import React from 'react'; +import { useRequiredBoard } from '~/components/Board/context'; import { firstUpperCase } from '~/tools/shared/strings'; -import { useConfigContext } from '~/config/provider'; - export const BoardHeadOverride = () => { - const { config, name } = useConfigContext(); + const board = useRequiredBoard(); + const { metaTitle, faviconImageUrl } = board; - if (!config || !name) return null; - - const { metaTitle, faviconUrl } = config.settings.customization; - const fallbackTitle = `${firstUpperCase(name)} Board • Homarr`; + const fallbackTitle = `${firstUpperCase(board.name)} Board • Homarr`; const title = metaTitle && metaTitle.length > 0 ? metaTitle : fallbackTitle; return ( @@ -18,11 +14,11 @@ export const BoardHeadOverride = () => { {title} - {faviconUrl && faviconUrl.length > 0 && ( + {faviconImageUrl && faviconImageUrl.length > 0 && ( <> - + - + )} diff --git a/src/components/layout/Templates/BoardLayout.tsx b/src/components/layout/Templates/BoardLayout.tsx index cf91750e1..6e9e3c78b 100644 --- a/src/components/layout/Templates/BoardLayout.tsx +++ b/src/components/layout/Templates/BoardLayout.tsx @@ -14,12 +14,11 @@ import { useSession } from 'next-auth/react'; import { Trans, useTranslation } from 'next-i18next'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useRequiredBoard } from '~/components/Board/context'; import { useEditModeStore } from '~/components/Dashboard/Views/useEditModeStore'; import { useNamedWrapperColumnCount } from '~/components/Dashboard/Wrappers/gridstack/store'; import { BoardHeadOverride } from '~/components/layout/Meta/BoardHeadOverride'; import { HeaderActionButton } from '~/components/layout/header/ActionButton'; -import { useConfigContext } from '~/config/provider'; -import { useScreenLargerThan } from '~/hooks/useScreenLargerThan'; import { api } from '~/utils/api'; import { MainLayout } from './MainLayout'; @@ -30,7 +29,7 @@ type BoardLayoutProps = { }; export const BoardLayout = ({ children, dockerEnabled }: BoardLayoutProps) => { - const { config } = useConfigContext(); + const board = useRequiredBoard(); const { data: session } = useSession(); return ( @@ -41,7 +40,7 @@ export const BoardLayout = ({ children, dockerEnabled }: BoardLayoutProps) => { {children} - + ); }; @@ -77,7 +76,7 @@ const DockerButton = () => { }; const CustomizeBoardButton = () => { - const { name } = useConfigContext(); + const { name } = useRequiredBoard(); const { t } = useTranslation('boards/common'); const href = useBoardLink(`/board/${name}/customize`); @@ -95,7 +94,8 @@ const editModeNotificationId = 'toggle-edit-mode'; const ToggleEditModeButton = () => { const { enabled, toggleEditMode } = useEditModeStore(); - const { config, name: configName } = useConfigContext(); + const board = useRequiredBoard(); + const { name } = board; const { mutateAsync: saveConfig } = api.config.save.useMutation(); const namedWrapperColumnCount = useNamedWrapperColumnCount(); const { t } = useTranslation(['layout/header/actions/toggle-edit-mode', 'common']); @@ -118,9 +118,9 @@ const ToggleEditModeButton = () => { const save = async () => { toggleEditMode(); - if (!config || !configName) return; - await saveConfig({ name: configName, config }); - Consola.log('Saved config to server', configName); + if (!board || !name) return; + await saveConfig({ name, config: {} as any }); + Consola.log('Saved config to server', name); hideNotification(editModeNotificationId); }; @@ -209,9 +209,9 @@ const AddElementButton = () => { }; const BackgroundImage = () => { - const { config } = useConfigContext(); + const board = useRequiredBoard(); - if (!config?.settings.customization.backgroundImageUrl) { + if (!board.backgroundImageUrl) { return null; } @@ -220,7 +220,7 @@ const BackgroundImage = () => { styles={{ body: { minHeight: '100vh', - backgroundImage: `url('${config?.settings.customization.backgroundImageUrl}')`, + backgroundImage: `url('${board.backgroundImageUrl}')`, backgroundPosition: 'center center', backgroundSize: 'cover', backgroundRepeat: 'no-repeat', diff --git a/src/components/layout/header/Search.tsx b/src/components/layout/header/Search.tsx index e0de950b2..1364d7067 100644 --- a/src/components/layout/header/Search.tsx +++ b/src/components/layout/header/Search.tsx @@ -12,7 +12,7 @@ import { useSession } from 'next-auth/react'; import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import { ReactNode, forwardRef, useMemo, useRef, useState } from 'react'; -import { useConfigContext } from '~/config/provider'; +import { AppItem, useOptionalBoard } from '~/components/Board/context'; import { api } from '~/utils/api'; import { MovieModal } from './Search/MovieModal'; @@ -31,19 +31,22 @@ export const Search = ({ isMobile, autoFocus }: SearchProps) => { const { data: userWithSettings } = api.user.withSettings.useQuery(undefined, { enabled: !!sessionData?.user, }); - const { config } = useConfigContext(); + const board = useOptionalBoard(); const { colors } = useMantineTheme(); const router = useRouter(); const [showMovieModal, movieModal] = useDisclosure(router.query.movie === 'true'); - const apps = useConfigApps(search); + const apps = useBoardApps(search); const engines = generateEngines( search, userWithSettings?.settings.searchTemplate ?? 'https://www.google.com/search?q=%s' ) .filter( (engine) => - engine.sort !== 'movie' || config?.apps.some((app) => app.integration.type === engine.value) + engine.sort !== 'movie' || + board?.sections.some((section) => + section.items.some((x) => x.type === 'app' && x.integration?.type === engine.value) + ) ) .map((engine) => ({ ...engine, @@ -139,25 +142,27 @@ const getItemComponent = (icon: SearchAutoCompleteItem['icon']) => { ); }; -const useConfigApps = (search: string) => { - const { config } = useConfigContext(); +const useBoardApps = (search: string) => { + const board = useOptionalBoard(); return useMemo(() => { if (search.trim().length === 0) return []; - const apps = config?.apps.filter((app) => - app.name.toLowerCase().includes(search.toLowerCase()) - ); - return ( - apps?.map((app) => ({ - icon: app.appearance.iconUrl, - label: app.name, - value: app.name, - sort: 'app', - metaData: { - url: app.behaviour.externalUrl, - }, - })) ?? [] - ); - }, [search, config]); + if (!board) return []; + const apps = board.sections + .flatMap((section) => section.items.filter((item) => item.type === 'app')) + .filter( + (x): x is AppItem => x.type === 'app' && x.name.toLowerCase().includes(search.toLowerCase()) + ); + + return apps.map((app) => ({ + icon: app.iconUrl, + label: app.name, + value: app.name, + sort: 'app', + metaData: { + url: app.externalUrl, + }, + })); + }, [search, board]); }; type SearchAutoCompleteItem = { diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 566e02054..b3ce556fc 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -16,24 +16,24 @@ import { AppProps } from 'next/app'; import { useEffect, useState } from 'react'; import 'video.js/dist/video-js.css'; import { CommonHead } from '~/components/layout/Meta/CommonHead'; +import { ConfigProvider } from '~/config/provider'; import { env } from '~/env.js'; import { ColorSchemeProvider } from '~/hooks/use-colorscheme'; import { modals } from '~/modals'; +import { ColorTheme } from '~/tools/color'; import { getLanguageByCode } from '~/tools/language'; +import { + ServerSidePackageAttributesType, + getServiceSidePackageAttributes, +} from '~/tools/server/getPackageVersion'; +import { theme } from '~/tools/server/theme/theme'; import { ConfigType } from '~/types/config'; import { api } from '~/utils/api'; import { colorSchemeParser } from '~/validations/user'; import { COOKIE_COLOR_SCHEME_KEY, COOKIE_LOCALE_KEY } from '../../data/constants'; import nextI18nextConfig from '../../next-i18next.config.js'; -import { ConfigProvider } from '~/config/provider'; import '../styles/global.scss'; -import { ColorTheme } from '~/tools/color'; -import { - ServerSidePackageAttributesType, - getServiceSidePackageAttributes, -} from '~/tools/server/getPackageVersion'; -import { theme } from '~/tools/server/theme/theme'; dayjs.extend(locale); dayjs.extend(utc); @@ -69,6 +69,7 @@ function App( const [primaryShade, setPrimaryShade] = useState( props.pageProps.primaryShade ?? 6 ); + const colorTheme = { primaryColor, secondaryColor, diff --git a/src/pages/board/[slug].tsx b/src/pages/board/[slug].tsx index 567abd141..2b9adeffc 100644 --- a/src/pages/board/[slug].tsx +++ b/src/pages/board/[slug].tsx @@ -1,7 +1,7 @@ import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; import { SSRConfig } from 'next-i18next'; import { z } from 'zod'; -import { Dashboard } from '~/components/Dashboard/Dashboard'; +import { Board } from '~/components/Dashboard/Dashboard'; import { BoardLayout } from '~/components/layout/Templates/BoardLayout'; import { useInitConfig } from '~/config/init'; import { env } from '~/env'; @@ -21,7 +21,7 @@ export default function BoardPage({ return ( - + ); } diff --git a/src/pages/board/index.tsx b/src/pages/board/index.tsx index 4a9c1223f..8faf32959 100644 --- a/src/pages/board/index.tsx +++ b/src/pages/board/index.tsx @@ -1,7 +1,8 @@ +import { TRPCError } from '@trpc/server'; import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; import { SSRConfig } from 'next-i18next'; -import { createContext, useContext } from 'react'; -import { Dashboard } from '~/components/Dashboard/Dashboard'; +import { BoardProvider } from '~/components/Board/context'; +import { Board } from '~/components/Dashboard/Dashboard'; import { BoardLayout } from '~/components/layout/Templates/BoardLayout'; import { env } from '~/env'; import { createTrpcServersideHelpers } from '~/server/api/helper'; @@ -9,62 +10,23 @@ import { getServerAuthSession } from '~/server/auth'; import { getDefaultBoardAsync } from '~/server/db/queries/userSettings'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; import { boardNamespaces } from '~/tools/server/translation-namespaces'; -import { RouterOutputs, api } from '~/utils/api'; - -type BoardContextType = { - boardName: string; - layout?: string; - board: RouterOutputs['boards']['byName']; -}; -const BoardContext = createContext(null!); -type BoardProviderProps = { - boardName: string; - layout?: string; - children: React.ReactNode; -}; -const BoardProvider = ({ children, ...props }: BoardProviderProps) => { - const { data: board } = api.boards.byName.useQuery(props); - - return ( - - {children} - - ); -}; - -const useBoard = () => { - const ctx = useContext(BoardContext); - if (!ctx) throw new Error('useBoard must be used within a BoardProvider'); - return ctx.board; -}; - -const Data = () => { - const board = useBoard(); - - return
{JSON.stringify(board, null, 2)}
; -}; +import { RouterOutputs } from '~/utils/api'; export default function BoardPage({ - boardName, + board, dockerEnabled, }: InferGetServerSidePropsType) { return ( - + - - + ); } type BoardGetServerSideProps = { - boardName: string; + board: RouterOutputs['boards']['byName']; dockerEnabled: boolean; _nextI18Next?: SSRConfig['_nextI18Next']; }; @@ -81,8 +43,18 @@ export const getServerSideProps: GetServerSideProps = a ); const helpers = await createTrpcServersideHelpers(ctx); - await helpers.boards.byName.prefetch({ boardName }); - const board = await helpers.boards.byNameSimple.fetch({ boardName }); + const board = await helpers.boards.byName.fetch({ boardName }).catch((err) => { + if (err instanceof TRPCError && err.code === 'NOT_FOUND') { + return null; + } + throw err; + }); + + if (!board) { + return { + notFound: true, + }; + } if (!board.allowGuests && !session?.user) { return { @@ -92,7 +64,7 @@ export const getServerSideProps: GetServerSideProps = a return { props: { - boardName, + board, primaryColor: board.primaryColor, secondaryColor: board.secondaryColor, primaryShade: board.primaryShade, diff --git a/src/pages/manage/index.tsx b/src/pages/manage/index.tsx index 6b89ef760..f378ff94b 100644 --- a/src/pages/manage/index.tsx +++ b/src/pages/manage/index.tsx @@ -1,12 +1,14 @@ import { Box, + Button, Card, Group, + Image, SimpleGrid, Stack, Text, + TextInput, Title, - Image, UnstyledButton, createStyles, } from '@mantine/core'; @@ -16,11 +18,13 @@ import { useSession } from 'next-auth/react'; import { useTranslation } from 'next-i18next'; import Head from 'next/head'; import Link from 'next/link'; +import { useState } from 'react'; import { ManageLayout } from '~/components/layout/Templates/ManageLayout'; import { useScreenLargerThan } from '~/hooks/useScreenLargerThan'; import { getServerAuthSession } from '~/server/auth'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; import { OnlyKeysWithStructure } from '~/types/helpers'; +import { api } from '~/utils/api'; import { type quickActions } from '../../../public/locales/en/manage/index.json'; @@ -64,6 +68,8 @@ const ManagementPage = () => { + + {t('quickActions.title')} @@ -156,3 +162,34 @@ const useStyles = createStyles((theme) => ({ }, }, })); + +const AddBoardCard = () => { + const { mutate } = api.boards.exampleBoard.useMutation(); + const [value, setValue] = useState(''); + + const handleClick = () => { + if (!value) return; + mutate( + { boardName: value }, + { + onSuccess: () => { + setValue(''); + }, + } + ); + }; + + return ( + + + setValue(e.currentTarget.value)} + placeholder="Boardname" + /> + + + + ); +}; diff --git a/src/server/api/routers/board.ts b/src/server/api/routers/board.ts index f1dbd948d..10b234cdd 100644 --- a/src/server/api/routers/board.ts +++ b/src/server/api/routers/board.ts @@ -1,10 +1,21 @@ import { TRPCError } from '@trpc/server'; +import { randomUUID } from 'crypto'; import { eq, inArray } from 'drizzle-orm'; import fs from 'fs'; import { z } from 'zod'; import { db } from '~/server/db'; +import { WidgetType } from '~/server/db/items'; import { getDefaultBoardAsync } from '~/server/db/queries/userSettings'; -import { boards, layoutItems, layouts, sections } from '~/server/db/schema'; +import { + appItems, + apps, + boards, + items, + layoutItems, + layouts, + sections, + widgets, +} from '~/server/db/schema'; import { configExists } from '~/tools/config/configExists'; import { getConfig } from '~/tools/config/getConfig'; import { getFrontendConfig } from '~/tools/config/getFrontendConfig'; @@ -142,14 +153,209 @@ export const boardRouter = createTRPCRouter({ message: 'Board not found', }); } - - console.log(board); return board; }), + exampleBoard: protectedProcedure + .input(z.object({ boardName: configNameSchema })) + .mutation(async ({ input, ctx }) => { + const boardId = randomUUID(); + const layoutId = randomUUID(); + const sectionId = randomUUID(); + await db.insert(boards).values({ + id: boardId, + name: input.boardName, + ownerId: ctx.session.user.id, + }); + + await db.insert(layouts).values({ + id: layoutId, + name: 'default', + boardId, + }); + + await db.insert(sections).values({ + id: sectionId, + layoutId, + type: 'empty', + position: 0, + }); + + await addApp({ + boardId, + sectionId, + name: 'Contribute', + internalUrl: 'https://github.com/ajnart/homarr', + iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/github.png', + width: 2, + height: 1, + x: 2, + y: 0, + }); + + await addApp({ + boardId, + sectionId, + name: 'Discord', + internalUrl: 'https://discord.com/invite/aCsmEV5RgA', + iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/discord.png', + width: 2, + height: 1, + x: 4, + y: 0, + }); + + await addApp({ + boardId, + sectionId, + name: 'Donate', + internalUrl: 'https://ko-fi.com/ajnart', + iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/ko-fi.png', + width: 2, + height: 1, + x: 6, + y: 0, + }); + + await addApp({ + boardId, + sectionId, + name: 'Documentation', + internalUrl: 'https://homarr.dev', + iconUrl: '/imgs/logo/logo.png', + width: 2, + height: 2, + x: 6, + y: 1, + }); + + await addWidget({ + boardId, + sectionId, + type: 'weather', + width: 2, + height: 1, + x: 0, + y: 0, + }); + + await addWidget({ + boardId, + sectionId, + type: 'date', + width: 2, + height: 1, + x: 8, + y: 0, + }); + + await addWidget({ + boardId, + sectionId, + type: 'date', + width: 2, + height: 1, + x: 8, + y: 1, + }); + + await addWidget({ + boardId, + sectionId, + type: 'notebook', + width: 6, + height: 3, + x: 0, + y: 1, + }); + }), }); +type AddWidgetProps = { + boardId: string; + sectionId: string; + type: WidgetType; + width: number; + height: number; + x: number; + y: number; +}; + +const addWidget = async ({ boardId, sectionId, type, ...positionProps }: AddWidgetProps) => { + const itemId = randomUUID(); + await db.insert(items).values({ + id: itemId, + type: 'widget', + boardId, + }); + + const widgetId = randomUUID(); + await db.insert(widgets).values({ + id: widgetId, + type, + itemId, + }); + + const layoutItemId = randomUUID(); + await db.insert(layoutItems).values({ + id: layoutItemId, + itemId, + sectionId, + ...positionProps, + }); +}; + +type AddAppProps = { + boardId: string; + sectionId: string; + name: string; + internalUrl: string; + iconUrl: string; + width: number; + height: number; + x: number; + y: number; +}; + +const addApp = async ({ + boardId, + sectionId, + iconUrl, + internalUrl, + name, + ...positionProps +}: AddAppProps) => { + const itemId = randomUUID(); + await db.insert(items).values({ + id: itemId, + type: 'app', + boardId, + }); + + const appId = randomUUID(); + await db.insert(apps).values({ + id: appId, + name, + internalUrl, + iconUrl, + }); + + await db.insert(appItems).values({ + appId: appId, + itemId: itemId, + }); + + const layoutItemId = randomUUID(); + await db.insert(layoutItems).values({ + id: layoutItemId, + itemId, + sectionId, + ...positionProps, + }); +}; + const getAppsForSectionsAsync = async (sectionIds: string[]) => { if (sectionIds.length === 0) return []; + return await db.query.appItems.findMany({ with: { app: { @@ -237,7 +443,7 @@ const mapSection = ( items: (ReturnType | ReturnType)[] ) => { const { layoutId, ...withoutLayoutId } = section; - if (section.type === 'empty') { + if (withoutLayoutId.type === 'empty') { const { name, position, type, ...sectionProps } = withoutLayoutId; return { ...sectionProps, @@ -246,7 +452,7 @@ const mapSection = ( items, }; } - if (section.type === 'hidden') { + if (withoutLayoutId.type === 'hidden') { const { name, position, type, ...sectionProps } = withoutLayoutId; return { ...sectionProps, @@ -255,7 +461,7 @@ const mapSection = ( items, }; } - if (section.type === 'category') { + if (withoutLayoutId.type === 'category') { const { name, position, type, ...sectionProps } = withoutLayoutId; return { ...sectionProps, diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 5b4cc699c..450c5f3fb 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -130,7 +130,7 @@ export const boards = sqliteTable('board', { backgroundImageUrl: text('background_image_url'), primaryColor: text('primary_color'), secondaryColor: text('secondary_color'), - primaryShade: text('primary_shade'), + primaryShade: int('primary_shade'), appOpacity: int('app_opacity'), customCss: text('custom_css'), @@ -165,7 +165,9 @@ export const integrationSecrets = sqliteTable( export const widgets = sqliteTable('widget', { id: text('id').notNull().primaryKey(), type: text('type').$type().notNull(), - itemId: text('item_id').notNull(), + itemId: text('item_id') + .notNull() + .references(() => items.id, { onDelete: 'cascade' }), }); export const widgetOptions = sqliteTable( @@ -205,7 +207,7 @@ export const appItems = sqliteTable('app_item', { openInNewTab: int('open_in_new_tab', { mode: 'boolean' }).notNull().default(false), isPingEnabled: int('is_ping_enabled', { mode: 'boolean' }).notNull().default(false), fontSize: int('font_size').notNull().default(16), - namePosition: text('name_position').$type().notNull().default('right'), + namePosition: text('name_position').$type().notNull().default('top'), nameStyle: text('name_style').$type().notNull().default('show'), nameLineClamp: int('name_line_clamp').notNull().default(1), appId: text('app_id') @@ -238,8 +240,12 @@ export const appStatusCodes = sqliteTable( export const layoutItems = sqliteTable('layout_item', { id: text('id').notNull().primaryKey(), - sectionId: text('section_id').notNull(), - itemId: text('item_id').notNull(), + sectionId: text('section_id') + .notNull() + .references(() => sections.id, { onDelete: 'cascade' }), + itemId: text('item_id') + .notNull() + .references(() => items.id, { onDelete: 'cascade' }), x: int('x').notNull(), y: int('y').notNull(), width: int('width').notNull(), diff --git a/src/tools/server/loginBuilder.ts b/src/tools/server/loginBuilder.ts index 5120a6dc9..6ea53668f 100644 --- a/src/tools/server/loginBuilder.ts +++ b/src/tools/server/loginBuilder.ts @@ -10,10 +10,9 @@ import { ParsedUrlQuery } from 'querystring'; export const checkForSessionOrAskForLogin = ( context: GetServerSidePropsContext, session: Session | null, - accessCallback: () => boolean, + accessCallback: () => boolean ): GetServerSidePropsResult | undefined => { if (!session?.user) { - console.log('detected logged out user!'); return { props: {}, redirect: { @@ -26,8 +25,8 @@ export const checkForSessionOrAskForLogin = ( if (!accessCallback()) { return { props: {}, - notFound: true - } + notFound: true, + }; } return undefined; diff --git a/src/widgets/WidgetWrapper.tsx b/src/widgets/WidgetWrapper.tsx index ec613edad..b94cfd2cc 100644 --- a/src/widgets/WidgetWrapper.tsx +++ b/src/widgets/WidgetWrapper.tsx @@ -1,23 +1,24 @@ import { ComponentType } from 'react'; - -import Widgets from '.'; +import { WidgetItem } from '~/components/Board/context'; import { HomarrCardWrapper } from '~/components/Dashboard/Tiles/HomarrCardWrapper'; import { WidgetsMenu } from '~/components/Dashboard/Tiles/Widgets/WidgetsMenu'; + +import Widgets from '.'; import ErrorBoundary from './boundary'; import { IWidget } from './widgets'; interface WidgetWrapperProps { widgetType: string; - widget: IWidget; + widget: WidgetItem; className: string; - WidgetComponent: ComponentType<{ widget: IWidget }>; + WidgetComponent: ComponentType<{ widget: WidgetItem }>; } // If a property has no value, set it to the default value -const useWidget = >(widget: T): T => { - const definition = Widgets[widget.type as keyof typeof Widgets]; +const useWidget = (widget: T): T => { + const definition = Widgets[widget.sort]; - const newProps = { ...widget.properties }; + const newProps = { ...widget.options }; Object.entries(definition.options).forEach(([key, option]) => { if (newProps[key] == null) { @@ -27,7 +28,7 @@ const useWidget = >(widget: T): T => { return { ...widget, - properties: newProps, + options: newProps, }; }; diff --git a/src/widgets/date/DateTile.tsx b/src/widgets/date/DateTile.tsx index 1b3ce6f77..edbc88f39 100644 --- a/src/widgets/date/DateTile.tsx +++ b/src/widgets/date/DateTile.tsx @@ -6,12 +6,12 @@ import timezones from 'dayjs/plugin/timezone'; import utc from 'dayjs/plugin/utc'; import { useSession } from 'next-auth/react'; import { useEffect, useRef, useState } from 'react'; +import { useSetSafeInterval } from '~/hooks/useSetSafeInterval'; import { getLanguageByCode } from '~/tools/language'; import { api } from '~/utils/api'; -import { useSetSafeInterval } from '~/hooks/useSetSafeInterval'; import { defineWidget } from '../helper'; -import { IWidget } from '../widgets'; +import { IWidget, InferWidget } from '../widgets'; dayjs.extend(utc); dayjs.extend(timezones); @@ -67,7 +67,7 @@ const definition = defineWidget({ component: DateTile, }); -export type IDateWidget = IWidget<(typeof definition)['id'], typeof definition>; +export type IDateWidget = InferWidget; interface DateTileProps { widget: IDateWidget; @@ -75,41 +75,41 @@ interface DateTileProps { function DateTile({ widget }: DateTileProps) { const date = useDateState( - widget.properties.enableTimezone ? widget.properties.timezoneLocation : undefined + widget.options.enableTimezone ? widget.options.timezoneLocation : undefined ); - const formatString = widget.properties.display24HourFormat ? 'HH:mm' : 'h:mm A'; + const formatString = widget.options.display24HourFormat ? 'HH:mm' : 'h:mm A'; const { ref, width } = useElementSize(); const { cx, classes } = useStyles(); return ( - {widget.properties.enableTimezone && widget.properties.titleState !== 'none' && ( + {widget.options.enableTimezone && widget.options.titleState !== 'none' && ( - {widget.properties.timezoneLocation.name} - {widget.properties.titleState === 'both' && dayjs(date).format(' (z)')} + {widget.options.timezoneLocation.name} + {widget.options.titleState === 'both' && dayjs(date).format(' (z)')} )} {dayjs(date).format(formatString)} - {!widget.properties.dateFormat.includes('hide') && ( + {!widget.options.dateFormat.includes('hide') && ( - {dayjs(date).format(widget.properties.dateFormat)} + {dayjs(date).format(widget.options.dateFormat)} )} ); } -const useStyles = createStyles(()=>({ - wrapper:{ +const useStyles = createStyles(() => ({ + wrapper: { display: 'flex', flexDirection: 'column', justifyContent: 'space-evenly', @@ -117,17 +117,17 @@ const useStyles = createStyles(()=>({ height: '100%', gap: 0, }, - clock:{ + clock: { lineHeight: '1', whiteSpace: 'nowrap', fontWeight: 700, fontSize: '2.125rem', }, - extras:{ + extras: { lineHeight: '1', whiteSpace: 'nowrap', - } -})) + }, +})); /** * State which updates when the minute is changing @@ -142,7 +142,7 @@ const useDateState = (location?: { latitude: number; longitude: number }) => { const { data: userWithSettings } = api.user.withSettings.useQuery(undefined, { enabled: !!sessionData?.user, }); - const userLanguage = userWithSettings?.settings.language; + const userLanguage = userWithSettings?.settings.language; const [date, setDate] = useState(getNewDate(timezone)); const setSafeInterval = useSetSafeInterval(); const timeoutRef = useRef(); // reference for initial timeout until first minute change diff --git a/src/widgets/notebook/NotebookEditor.tsx b/src/widgets/notebook/NotebookEditor.tsx index ce71e9f13..2e211ef58 100644 --- a/src/widgets/notebook/NotebookEditor.tsx +++ b/src/widgets/notebook/NotebookEditor.tsx @@ -5,12 +5,12 @@ import { IconEdit, IconEditOff } from '@tabler/icons-react'; import { BubbleMenu, useEditor } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import { useState } from 'react'; +import { useRequiredBoard } from '~/components/Board/context'; +import { useEditModeStore } from '~/components/Dashboard/Views/useEditModeStore'; import { useConfigStore } from '~/config/store'; import { useColorTheme } from '~/tools/color'; import { api } from '~/utils/api'; -import { useEditModeStore } from '~/components/Dashboard/Views/useEditModeStore'; -import { useConfigContext } from '~/config/provider'; import { WidgetLoading } from '../loading'; import { INotebookWidget } from './NotebookWidgetTile'; @@ -19,12 +19,12 @@ Link.configure({ }); export function Editor({ widget }: { widget: INotebookWidget }) { - const [content, setContent] = useState(widget.properties.content); + const [content, setContent] = useState(widget.options.content); const { enabled } = useEditModeStore(); const [isEditing, setIsEditing] = useState(false); - const { config, name: configName } = useConfigContext(); + const board = useRequiredBoard(); const updateConfig = useConfigStore((x) => x.updateConfig); const { primaryColor } = useColorTheme(); @@ -47,7 +47,7 @@ export function Editor({ widget }: { widget: INotebookWidget }) { editor.setEditable(current); updateConfig( - configName!, + board.name!, (previous) => { const currentWidget = previous.widgets.find((x) => x.id === widget.id); currentWidget!.properties.content = debouncedContent; @@ -64,7 +64,7 @@ export function Editor({ widget }: { widget: INotebookWidget }) { ); void mutateAsync({ - configName: configName!, + configName: board.name!, content: debouncedContent, widgetId: widget.id, }); @@ -72,7 +72,7 @@ export function Editor({ widget }: { widget: INotebookWidget }) { return current; }; - if (!config || !configName) return ; + if (!board) return ; return ( <> @@ -104,7 +104,7 @@ export function Editor({ widget }: { widget: INotebookWidget }) { > diff --git a/src/widgets/notebook/NotebookWidgetTile.tsx b/src/widgets/notebook/NotebookWidgetTile.tsx index 5b52f7f4b..7d6e2b612 100644 --- a/src/widgets/notebook/NotebookWidgetTile.tsx +++ b/src/widgets/notebook/NotebookWidgetTile.tsx @@ -1,9 +1,8 @@ import { IconNotes } from '@tabler/icons-react'; -import { useTranslation } from 'next-i18next'; import dynamic from 'next/dynamic'; import { defineWidget } from '../helper'; -import { IWidget } from '../widgets'; +import { InferWidget } from '../widgets'; const Editor = dynamic(() => import('./NotebookEditor').then((module) => module.Editor), { ssr: false, @@ -34,7 +33,7 @@ const definition = defineWidget({ export default definition; -export type INotebookWidget = IWidget<(typeof definition)['id'], typeof definition>; +export type INotebookWidget = InferWidget; interface NotebookWidgetProps { widget: INotebookWidget; diff --git a/src/widgets/weather/WeatherTile.tsx b/src/widgets/weather/WeatherTile.tsx index 8514bbcfc..e9389ff8f 100644 --- a/src/widgets/weather/WeatherTile.tsx +++ b/src/widgets/weather/WeatherTile.tsx @@ -6,12 +6,12 @@ import { IconCloudRain, IconMapPin, } from '@tabler/icons-react'; +import { useTranslation } from 'react-i18next'; import { api } from '~/utils/api'; import { defineWidget } from '../helper'; -import { IWidget } from '../widgets'; +import { IWidget, InferWidget } from '../widgets'; import { WeatherIcon } from './WeatherIcon'; -import { useTranslation } from 'react-i18next'; const definition = defineWidget({ id: 'weather', @@ -43,14 +43,14 @@ const definition = defineWidget({ component: WeatherTile, }); -export type IWeatherWidget = IWidget<(typeof definition)['id'], typeof definition>; +export type IWeatherWidget = InferWidget; interface WeatherTileProps { widget: IWeatherWidget; } function WeatherTile({ widget }: WeatherTileProps) { - const { data: weather, isLoading, isError } = api.weather.at.useQuery(widget.properties.location); + const { data: weather, isLoading, isError } = api.weather.at.useQuery(widget.options.location); const { width, ref } = useElementSize(); const { t } = useTranslation('modules/weather'); @@ -100,32 +100,23 @@ function WeatherTile({ widget }: WeatherTileProps) { > - {getPerferedUnit( - weather.current_weather.temperature, - widget.properties.displayInFahrenheit - )} + {getPerferedUnit(weather.current_weather.temperature, widget.options.displayInFahrenheit)} {width > 200 && ( - {getPerferedUnit( - weather.daily.temperature_2m_max[0], - widget.properties.displayInFahrenheit - )} + {getPerferedUnit(weather.daily.temperature_2m_max[0], widget.options.displayInFahrenheit)} - {getPerferedUnit( - weather.daily.temperature_2m_min[0], - widget.properties.displayInFahrenheit - )} + {getPerferedUnit(weather.daily.temperature_2m_min[0], widget.options.displayInFahrenheit)} )} - {widget.properties.displayCityName && ( + {widget.options.displayCityName && ( - {widget.properties.location.name} + {widget.options.location.name} )} diff --git a/src/widgets/widgets.ts b/src/widgets/widgets.ts index 3763fe61d..6c2723b62 100644 --- a/src/widgets/widgets.ts +++ b/src/widgets/widgets.ts @@ -8,7 +8,7 @@ import { } from '@mantine/core'; import { Icon } from '@tabler/icons-react'; import React from 'react'; - +import { WidgetItem } from '~/components/Board/context'; import { AreaType } from '~/types/area'; import { ShapeType } from '~/types/shape'; @@ -25,6 +25,14 @@ export type IWidget shape: ShapeType; }; +export type InferWidget = WidgetItem & { + options: { + [key in keyof TDefinition['options']]: MakeLessSpecific< + TDefinition['options'][key]['defaultValue'] + >; + }; +}; + // Makes the type less specific // For example when the type true is used as input the result is boolean // By not using this type the definition would always be { property: true } diff --git a/yarn.lock b/yarn.lock index 9138abab3..de22c0559 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1030,7 +1030,7 @@ __metadata: languageName: node linkType: hard -"@mantine/core@npm:^6.0.0": +"@mantine/core@npm:^6.0.21": version: 6.0.21 resolution: "@mantine/core@npm:6.0.21" dependencies: @@ -1048,7 +1048,7 @@ __metadata: languageName: node linkType: hard -"@mantine/dates@npm:^6.0.0": +"@mantine/dates@npm:^6.0.21": version: 6.0.21 resolution: "@mantine/dates@npm:6.0.21" dependencies: @@ -1062,7 +1062,7 @@ __metadata: languageName: node linkType: hard -"@mantine/dropzone@npm:^6.0.0": +"@mantine/dropzone@npm:^6.0.21": version: 6.0.21 resolution: "@mantine/dropzone@npm:6.0.21" dependencies: @@ -1077,7 +1077,7 @@ __metadata: languageName: node linkType: hard -"@mantine/form@npm:^6.0.0": +"@mantine/form@npm:^6.0.21": version: 6.0.21 resolution: "@mantine/form@npm:6.0.21" dependencies: @@ -1089,7 +1089,7 @@ __metadata: languageName: node linkType: hard -"@mantine/hooks@npm:^6.0.0": +"@mantine/hooks@npm:^6.0.21": version: 6.0.21 resolution: "@mantine/hooks@npm:6.0.21" peerDependencies: @@ -1098,7 +1098,7 @@ __metadata: languageName: node linkType: hard -"@mantine/modals@npm:^6.0.0": +"@mantine/modals@npm:^6.0.21": version: 6.0.21 resolution: "@mantine/modals@npm:6.0.21" dependencies: @@ -1112,7 +1112,7 @@ __metadata: languageName: node linkType: hard -"@mantine/next@npm:^6.0.0": +"@mantine/next@npm:^6.0.21": version: 6.0.21 resolution: "@mantine/next@npm:6.0.21" dependencies: @@ -1126,7 +1126,7 @@ __metadata: languageName: node linkType: hard -"@mantine/notifications@npm:^6.0.0": +"@mantine/notifications@npm:^6.0.21": version: 6.0.21 resolution: "@mantine/notifications@npm:6.0.21" dependencies: @@ -1141,7 +1141,7 @@ __metadata: languageName: node linkType: hard -"@mantine/prism@npm:^6.0.19": +"@mantine/prism@npm:^6.0.21": version: 6.0.21 resolution: "@mantine/prism@npm:6.0.21" dependencies: @@ -1185,7 +1185,7 @@ __metadata: languageName: node linkType: hard -"@mantine/tiptap@npm:^6.0.17": +"@mantine/tiptap@npm:^6.0.21": version: 6.0.21 resolution: "@mantine/tiptap@npm:6.0.21" dependencies: @@ -7566,16 +7566,16 @@ __metadata: "@emotion/react": ^11.10.6 "@emotion/server": ^11.10.0 "@jellyfin/sdk": ^0.8.0 - "@mantine/core": ^6.0.0 - "@mantine/dates": ^6.0.0 - "@mantine/dropzone": ^6.0.0 - "@mantine/form": ^6.0.0 - "@mantine/hooks": ^6.0.0 - "@mantine/modals": ^6.0.0 - "@mantine/next": ^6.0.0 - "@mantine/notifications": ^6.0.0 - "@mantine/prism": ^6.0.19 - "@mantine/tiptap": ^6.0.17 + "@mantine/core": ^6.0.21 + "@mantine/dates": ^6.0.21 + "@mantine/dropzone": ^6.0.21 + "@mantine/form": ^6.0.21 + "@mantine/hooks": ^6.0.21 + "@mantine/modals": ^6.0.21 + "@mantine/next": ^6.0.21 + "@mantine/notifications": ^6.0.21 + "@mantine/prism": ^6.0.21 + "@mantine/tiptap": ^6.0.21 "@next/bundle-analyzer": ^13.0.0 "@next/eslint-plugin-next": ^13.4.5 "@nivo/core": ^0.83.0