✨ Add board customization page
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import { Button, Container, Group, Paper, Stack, Text, Title } from '@mantine/core';
|
||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconBrush,
|
||||
IconChartCandle,
|
||||
IconCheck,
|
||||
IconDragDrop,
|
||||
IconLayout,
|
||||
IconX,
|
||||
TablerIconsProps,
|
||||
} from '@tabler/icons-react';
|
||||
import { GetServerSideProps } from 'next';
|
||||
@@ -12,6 +15,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { ReactNode } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { AppearanceCustomization } from '~/components/Board/Customize/Appearance/AppearanceCustomization';
|
||||
import { GridstackCustomization } from '~/components/Board/Customize/Gridstack/GridstackCustomization';
|
||||
import { LayoutCustomization } from '~/components/Board/Customize/Layout/LayoutCustomization';
|
||||
@@ -21,44 +25,91 @@ import {
|
||||
useBoardCustomizationForm,
|
||||
} from '~/components/Board/Customize/form';
|
||||
import { MainLayout } from '~/components/layout/main';
|
||||
import { createTrpcServersideHelpers } from '~/server/api/helper';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { dashboardNamespaces } from '~/tools/server/translation-namespaces';
|
||||
import { api } from '~/utils/api';
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
import { boardCustomizationSchema } from '~/validations/dashboards';
|
||||
|
||||
const notificationId = 'board-customization-notification';
|
||||
|
||||
export default function CustomizationPage() {
|
||||
const query = useRouter().query as { slug: string };
|
||||
const { t } = useTranslation([
|
||||
'settings/customization/general',
|
||||
'settings/customization/color-selector',
|
||||
]);
|
||||
const utils = api.useContext();
|
||||
const { data: config } = api.config.byName.useQuery({ name: query.slug });
|
||||
const { mutateAsync: saveCusomization, isLoading } = api.config.saveCusomization.useMutation();
|
||||
const { i18nZodResolver } = useI18nZodResolver();
|
||||
const form = useBoardCustomizationForm({
|
||||
initialValues: {
|
||||
layout: {
|
||||
leftSidebarEnabled: false,
|
||||
rightSidebarEnabled: false,
|
||||
pingsEnabled: false,
|
||||
leftSidebarEnabled: config?.settings.customization.layout.enabledLeftSidebar ?? false,
|
||||
rightSidebarEnabled: config?.settings.customization.layout.enabledRightSidebar ?? false,
|
||||
pingsEnabled: config?.settings.customization.layout.enabledPing ?? false,
|
||||
},
|
||||
appearance: {
|
||||
backgroundSrc: '',
|
||||
primaryColor: 'red',
|
||||
secondaryColor: 'orange',
|
||||
shade: 8,
|
||||
opacity: 50,
|
||||
customCss: '',
|
||||
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: 3,
|
||||
md: 6,
|
||||
lg: 12,
|
||||
sm: config?.settings.customization.gridstack?.columnCountSmall ?? 3,
|
||||
md: config?.settings.customization.gridstack?.columnCountMedium ?? 6,
|
||||
lg: config?.settings.customization.gridstack?.columnCountLarge ?? 12,
|
||||
},
|
||||
pageMetadata: {
|
||||
pageTitle: '',
|
||||
metaTitle: '',
|
||||
logoSrc: '',
|
||||
faviconSrc: '',
|
||||
pageTitle: config?.settings.customization.pageTitle ?? '',
|
||||
metaTitle: config?.settings.customization.metaTitle ?? '',
|
||||
logoSrc: config?.settings.customization.logoImageUrl ?? '',
|
||||
faviconSrc: config?.settings.customization.faviconUrl ?? '',
|
||||
},
|
||||
},
|
||||
validate: i18nZodResolver(boardCustomizationSchema),
|
||||
});
|
||||
|
||||
const handleSubmit = async (values: z.infer<typeof boardCustomizationSchema>) => {
|
||||
if (isLoading) return;
|
||||
showNotification({
|
||||
id: notificationId,
|
||||
title: 'Saving customization',
|
||||
message: 'Please wait while we save your customization',
|
||||
loading: true,
|
||||
});
|
||||
await saveCusomization(
|
||||
{
|
||||
name: query.slug,
|
||||
...values,
|
||||
},
|
||||
{
|
||||
onSettled() {
|
||||
void utils.config.byName.invalidate({ name: query.slug });
|
||||
},
|
||||
onSuccess() {
|
||||
updateNotification({
|
||||
id: notificationId,
|
||||
title: 'Customization saved',
|
||||
message: 'Your customization has been saved',
|
||||
color: 'green',
|
||||
icon: <IconCheck />,
|
||||
});
|
||||
},
|
||||
onError() {
|
||||
updateNotification({
|
||||
id: notificationId,
|
||||
title: 'Error',
|
||||
message: 'Unable to save customization',
|
||||
color: 'red',
|
||||
icon: <IconX />,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<Container>
|
||||
@@ -76,7 +127,7 @@ export default function CustomizationPage() {
|
||||
</Button>
|
||||
</Group>
|
||||
<BoardCustomizationFormProvider form={form}>
|
||||
<form>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack spacing="xl">
|
||||
<Stack spacing="xs">
|
||||
<SectionTitle type="layout" icon={IconLayout} />
|
||||
@@ -94,7 +145,9 @@ export default function CustomizationPage() {
|
||||
<SectionTitle type="appereance" icon={IconBrush} />
|
||||
<AppearanceCustomization />
|
||||
</Stack>
|
||||
<Button type="submit">Save changes</Button>
|
||||
<Button type="submit" loading={isLoading}>
|
||||
Save changes
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</BoardCustomizationFormProvider>
|
||||
@@ -124,11 +177,34 @@ const SectionTitle = ({ type, icon: Icon }: SectionTitleProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req, res, locale }) => {
|
||||
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 });
|
||||
|
||||
helpers.config.byName.prefetch({ name: routeParams.data.slug });
|
||||
|
||||
const translations = await getServerSideTranslations(dashboardNamespaces, locale, req, res);
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
...translations,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,13 @@ import { Button, ButtonProps, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { useHotkeys, useWindowEvent } from '@mantine/hooks';
|
||||
import { openContextModal } from '@mantine/modals';
|
||||
import { hideNotification, showNotification } from '@mantine/notifications';
|
||||
import { IconApps, IconBrandDocker, IconEditCircle, IconEditCircleOff } from '@tabler/icons-react';
|
||||
import {
|
||||
IconApps,
|
||||
IconBrandDocker,
|
||||
IconEditCircle,
|
||||
IconEditCircleOff,
|
||||
IconSettings,
|
||||
} from '@tabler/icons-react';
|
||||
import Consola from 'consola';
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||
import { useSession } from 'next-auth/react';
|
||||
@@ -75,6 +81,7 @@ export const HeaderActions = () => {
|
||||
<>
|
||||
<DockerButton />
|
||||
<ToggleEditModeButton />
|
||||
<CustomizeBoardButton />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -91,6 +98,18 @@ const DockerButton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const CustomizeBoardButton = () => {
|
||||
const { name } = useConfigContext();
|
||||
|
||||
return (
|
||||
<Tooltip label="Customize board">
|
||||
<HeaderActionButton component={Link} href={`/board/${name}/customize`}>
|
||||
<IconSettings size={20} stroke={1.5} />
|
||||
</HeaderActionButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
type SpecificLinkProps = {
|
||||
component: typeof Link;
|
||||
href: string;
|
||||
|
||||
Reference in New Issue
Block a user