import { Affix, Button, Card, Container, Group, Paper, Stack, Text, Title, Transition, rem, } from '@mantine/core'; import { showNotification, updateNotification } from '@mantine/notifications'; import { IconArrowLeft, IconBrush, IconChartCandle, IconCheck, IconDragDrop, IconLayout, IconLock, IconX, TablerIconsProps, } from '@tabler/icons-react'; import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; import { useTranslation } from 'next-i18next'; import Head from 'next/head'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { ReactNode } from 'react'; import { z } from 'zod'; import { AccessCustomization } from '~/components/Board/Customize/Access/AccessCustomization'; import { AppearanceCustomization } from '~/components/Board/Customize/Appearance/AppearanceCustomization'; import { GridstackCustomization } from '~/components/Board/Customize/Gridstack/GridstackCustomization'; import { LayoutCustomization } from '~/components/Board/Customize/Layout/LayoutCustomization'; import { PageMetadataCustomization } from '~/components/Board/Customize/PageMetadata/PageMetadataCustomization'; import { BoardCustomizationFormProvider, useBoardCustomizationForm, } from '~/components/Board/Customize/form'; import { useBoardLink } from '~/components/layout/Templates/BoardLayout'; import { MainLayout } from '~/components/layout/Templates/MainLayout'; import { createTrpcServersideHelpers } from '~/server/api/helper'; import { getServerAuthSession } from '~/server/auth'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; import { firstUpperCase } from '~/tools/shared/strings'; import { api } from '~/utils/api'; import { useI18nZodResolver } from '~/utils/i18n-zod-resolver'; import { boardCustomizationSchema } from '~/validations/boards'; const notificationId = 'board-customization-notification'; export default function CustomizationPage({ initialConfig, }: InferGetServerSidePropsType) { const query = useRouter().query as { slug: string; }; const utils = api.useContext(); const { data: config } = api.config.byName.useQuery( { name: query.slug }, { initialData: initialConfig, refetchOnMount: false, } ); const { mutateAsync: saveCusomization, isLoading } = api.config.saveCusomization.useMutation(); const { i18nZodResolver } = useI18nZodResolver(); const { t } = useTranslation('boards/customize'); const form = useBoardCustomizationForm({ initialValues: { access: { allowGuests: config?.settings.access.allowGuests ?? false, }, layout: { leftSidebarEnabled: config?.settings.customization.layout.enabledLeftSidebar ?? false, rightSidebarEnabled: config?.settings.customization.layout.enabledRightSidebar ?? false, pingsEnabled: config?.settings.customization.layout.enabledPing ?? false, }, appearance: { backgroundSrc: config?.settings.customization.backgroundImageUrl ?? '', primaryColor: config?.settings.customization.colors.primary ?? 'red', secondaryColor: config?.settings.customization.colors.secondary ?? 'orange', shade: (config?.settings.customization.colors.shade as number | undefined) ?? 8, opacity: config?.settings.customization.appOpacity ?? 50, customCss: config?.settings.customization.customCss ?? '', }, gridstack: { sm: config?.settings.customization.gridstack?.columnCountSmall ?? 3, md: config?.settings.customization.gridstack?.columnCountMedium ?? 6, lg: config?.settings.customization.gridstack?.columnCountLarge ?? 12, }, pageMetadata: { pageTitle: config?.settings.customization.pageTitle ?? '', metaTitle: config?.settings.customization.metaTitle ?? '', logoSrc: config?.settings.customization.logoImageUrl ?? '', faviconSrc: config?.settings.customization.faviconUrl ?? '', }, }, validate: i18nZodResolver(boardCustomizationSchema), validateInputOnChange: true, validateInputOnBlur: true, }); const backToBoardHref = useBoardLink(`/board/${query.slug}`); const handleSubmit = async (values: z.infer) => { if (isLoading) return; showNotification({ id: notificationId, title: t('notifications.pending.title'), message: t('notifications.pending.message'), loading: true, }); await saveCusomization( { name: query.slug, ...values, }, { onSettled() { void utils.config.byName.invalidate({ name: query.slug }); }, onSuccess() { updateNotification({ id: notificationId, title: t('notifications.success.title'), message: t('notifications.success.message'), color: 'green', icon: , }); form.resetDirty(); }, onError() { updateNotification({ id: notificationId, title: t('notifications.error.title'), message: t('notifications.error.message'), color: 'red', icon: , }); }, } ); }; const metaTitle = `${t('metaTitle', { name: firstUpperCase(query.slug), })} • Homarr`; return ( } > {t('backToBoard')} } > {metaTitle} {(transitionStyles) => ( ({ background: theme.colorScheme === 'dark' ? theme.colors.dark[8] : theme.colors.gray[1], })} shadow="md" withBorder > {t('save.note')} )} {t('pageTitle', { name: firstUpperCase(query.slug), })} ); } type SectionTitleProps = { type: 'layout' | 'gridstack' | 'pageMetadata' | 'appereance' | 'access'; icon: (props: TablerIconsProps) => ReactNode; }; const SectionTitle = ({ type, icon: Icon }: SectionTitleProps) => { const { t } = useTranslation('settings/customization/general'); return ( {t(`accordeon.${type}.name`)} {t(`accordeon.${type}.description`)} ); }; const routeParamsSchema = z.object({ slug: z.string(), }); export const getServerSideProps: GetServerSideProps = async ({ req, res, locale, params }) => { const routeParams = routeParamsSchema.safeParse(params); if (!routeParams.success) { return { notFound: true, }; } const session = await getServerAuthSession({ req, res }); if (!session?.user.isAdmin) { return { notFound: true, }; } const helpers = await createTrpcServersideHelpers({ req, res }); const config = await helpers.config.byName.fetch({ name: routeParams.data.slug }); const translations = await getServerSideTranslations( [ 'boards/customize', 'settings/common', 'settings/customization/general', 'settings/customization/page-appearance', 'settings/customization/shade-selector', 'settings/customization/opacity-selector', 'settings/customization/gridstack', 'settings/customization/access', ], locale, req, res ); return { props: { initialConfig: config, primaryColor: config.settings.customization.colors.primary, secondaryColor: config.settings.customization.colors.secondary, primaryShade: config.settings.customization.colors.shade, trpcState: helpers.dehydrate(), ...translations, }, }; };