mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 07:25:48 +01:00
🚧 Add board customization page
This commit is contained in:
@@ -0,0 +1,208 @@
|
|||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ColorSwatch,
|
||||||
|
Group,
|
||||||
|
Input,
|
||||||
|
Slider,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
createStyles,
|
||||||
|
rem,
|
||||||
|
useMantineTheme,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { highlight, languages } from 'prismjs';
|
||||||
|
import Editor from 'react-simple-code-editor';
|
||||||
|
|
||||||
|
import { useBoardCustomizationFormContext } from '../form';
|
||||||
|
|
||||||
|
export const AppearanceCustomization = () => {
|
||||||
|
const { t } = useTranslation([
|
||||||
|
'settings/customization/page-appearance',
|
||||||
|
'settings/customization/color-selector',
|
||||||
|
]);
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
const form = useBoardCustomizationFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing="sm">
|
||||||
|
<TextInput
|
||||||
|
label={t('background.label')}
|
||||||
|
placeholder="/imgs/backgrounds/background.png"
|
||||||
|
{...form.getInputProps('appearance.backgroundSrc')}
|
||||||
|
/>
|
||||||
|
<ColorSelector type="primaryColor" />
|
||||||
|
<ColorSelector type="secondaryColor" />
|
||||||
|
<ShadeSelector />
|
||||||
|
<OpacitySlider />
|
||||||
|
<CustomCssInput />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ColorSelectorProps = {
|
||||||
|
type: 'primaryColor' | 'secondaryColor';
|
||||||
|
};
|
||||||
|
const ColorSelector = ({ type }: ColorSelectorProps) => {
|
||||||
|
const { t } = useTranslation('settings/customization/color-selector');
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
const form = useBoardCustomizationFormContext();
|
||||||
|
|
||||||
|
const colors = Object.keys(theme.colors).map((color) => ({
|
||||||
|
swatch: theme.colors[color][6],
|
||||||
|
color,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input.Wrapper label={type}>
|
||||||
|
<Group>
|
||||||
|
{colors.map(({ color, swatch }) => (
|
||||||
|
<ColorSwatch
|
||||||
|
key={color}
|
||||||
|
component="button"
|
||||||
|
type="button"
|
||||||
|
onClick={() => form.getInputProps(`appearance.${type}`).onChange(color)}
|
||||||
|
color={swatch}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
{color === form.values.appearance[type] && <CheckIcon width={rem(10)} />}
|
||||||
|
</ColorSwatch>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Input.Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShadeSelector = () => {
|
||||||
|
const form = useBoardCustomizationFormContext();
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
|
const primaryColor = form.values.appearance.primaryColor;
|
||||||
|
const primaryShades = theme.colors[primaryColor].map((_, shade) => ({
|
||||||
|
swatch: theme.colors[primaryColor][shade],
|
||||||
|
shade,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input.Wrapper label="Shade">
|
||||||
|
<Group>
|
||||||
|
{primaryShades.map(({ shade, swatch }) => (
|
||||||
|
<ColorSwatch
|
||||||
|
key={shade}
|
||||||
|
component="button"
|
||||||
|
type="button"
|
||||||
|
onClick={() => form.getInputProps(`appearance.shade`).onChange(shade)}
|
||||||
|
color={swatch}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
{shade === form.values.appearance.shade && <CheckIcon width={rem(10)} />}
|
||||||
|
</ColorSwatch>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Input.Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OpacitySlider = () => {
|
||||||
|
const { t } = useTranslation('settings/customization/opacity-selector');
|
||||||
|
const form = useBoardCustomizationFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input.Wrapper label={t('label')} mb="sm">
|
||||||
|
<Slider
|
||||||
|
step={10}
|
||||||
|
min={10}
|
||||||
|
marks={opacityMarks}
|
||||||
|
styles={{ markLabel: { fontSize: 'xx-small' } }}
|
||||||
|
{...form.getInputProps('appearance.opacity')}
|
||||||
|
/>
|
||||||
|
</Input.Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const opacityMarks = [
|
||||||
|
{ value: 10, label: '10%' },
|
||||||
|
{ value: 20, label: '20%' },
|
||||||
|
{ value: 30, label: '30%' },
|
||||||
|
{ value: 40, label: '40%' },
|
||||||
|
{ value: 50, label: '50%' },
|
||||||
|
{ value: 60, label: '60%' },
|
||||||
|
{ value: 70, label: '70%' },
|
||||||
|
{ value: 80, label: '80%' },
|
||||||
|
{ value: 90, label: '90%' },
|
||||||
|
{ value: 100, label: '100%' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const CustomCssInput = () => {
|
||||||
|
const { t } = useTranslation('settings/customization/page-appearance');
|
||||||
|
const { classes } = useStyles();
|
||||||
|
const form = useBoardCustomizationFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input.Wrapper
|
||||||
|
label={t('customCSS.label')}
|
||||||
|
description={t('customCSS.description')}
|
||||||
|
inputWrapperOrder={['label', 'description', 'input', 'error']}
|
||||||
|
>
|
||||||
|
<div className={classes.codeEditorRoot}>
|
||||||
|
<Editor
|
||||||
|
{...form.getInputProps('appearance.customCss')}
|
||||||
|
onValueChange={(code) => form.getInputProps('appearance.customCss').onChange(code)}
|
||||||
|
highlight={(code) => highlight(code, languages.extend('css', {}), 'css')}
|
||||||
|
padding={10}
|
||||||
|
style={{
|
||||||
|
fontFamily: '"Fira code", "Fira Mono", monospace',
|
||||||
|
fontSize: 12,
|
||||||
|
minHeight: 250,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Input.Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ colors, colorScheme, radius }) => ({
|
||||||
|
codeEditorFooter: {
|
||||||
|
borderBottomLeftRadius: radius.sm,
|
||||||
|
borderBottomRightRadius: radius.sm,
|
||||||
|
backgroundColor: colorScheme === 'dark' ? colors.dark[7] : undefined,
|
||||||
|
},
|
||||||
|
codeEditorRoot: {
|
||||||
|
marginTop: 4,
|
||||||
|
borderColor: colorScheme === 'dark' ? colors.dark[4] : colors.gray[4],
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderRadius: radius.sm,
|
||||||
|
},
|
||||||
|
codeEditor: {
|
||||||
|
backgroundColor: colorScheme === 'dark' ? colors.dark[6] : 'white',
|
||||||
|
fontSize: 12,
|
||||||
|
|
||||||
|
'& ::placeholder': {
|
||||||
|
color: colorScheme === 'dark' ? colors.dark[3] : colors.gray[5],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
/*
|
||||||
|
<BackgroundChanger />
|
||||||
|
|
||||||
|
<Stack spacing="xs" my="md">
|
||||||
|
<Text>{t('settings/customization/color-selector:colors')}</Text>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col sm={12} md={6}>
|
||||||
|
<ColorSelector type="primary" defaultValue="red" />
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col sm={12} md={6}>
|
||||||
|
<ColorSelector type="secondary" defaultValue="orange" />
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col sm={12} md={6}>
|
||||||
|
<ShadeSelector />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<DashboardTilesOpacitySelector />
|
||||||
|
<CustomCssChanger />
|
||||||
|
*/
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Input, Slider } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { GridstackBreakpoints } from '~/constants/gridstack-breakpoints';
|
||||||
|
|
||||||
|
import { useBoardCustomizationFormContext } from '../form';
|
||||||
|
|
||||||
|
export const GridstackCustomization = () => {
|
||||||
|
const { t } = useTranslation('settings/customization/gridstack');
|
||||||
|
const form = useBoardCustomizationFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input.Wrapper
|
||||||
|
label={t('columnsCount.labelPreset', { size: t('common:breakPoints.small') })}
|
||||||
|
description={t('columnsCount.descriptionPreset', { pixels: GridstackBreakpoints.medium })}
|
||||||
|
mb="md"
|
||||||
|
>
|
||||||
|
<Slider min={1} max={8} mt="xs" {...form.getInputProps('gridstack.sm')} />
|
||||||
|
</Input.Wrapper>
|
||||||
|
<Input.Wrapper
|
||||||
|
label={t('columnsCount.labelPreset', { size: t('common:breakPoints.medium') })}
|
||||||
|
description={t('columnsCount.descriptionPreset', { pixels: GridstackBreakpoints.large })}
|
||||||
|
mb="md"
|
||||||
|
>
|
||||||
|
<Slider min={3} max={16} mt="xs" {...form.getInputProps('gridstack.md')} />
|
||||||
|
</Input.Wrapper>
|
||||||
|
<Input.Wrapper
|
||||||
|
label={t('columnsCount.labelPreset', { size: t('common:breakPoints.large') })}
|
||||||
|
description={t('columnsCount.descriptionExceedsPreset', {
|
||||||
|
pixels: GridstackBreakpoints.large,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Slider min={5} max={20} mt="xs" {...form.getInputProps('gridstack.lg')} />
|
||||||
|
</Input.Wrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Button, Checkbox, Grid, Stack } from '@mantine/core';
|
||||||
|
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
|
import { useBoardCustomizationFormContext } from '../form';
|
||||||
|
import { LayoutPreview } from './LayoutPreview';
|
||||||
|
|
||||||
|
export const LayoutCustomization = () => {
|
||||||
|
const { t } = useTranslation('settings/common');
|
||||||
|
const form = useBoardCustomizationFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid gutter="xl" align="stretch">
|
||||||
|
<Grid.Col span={12} sm={6}>
|
||||||
|
<LayoutPreview
|
||||||
|
showLeftSidebar={form.values.layout.leftSidebarEnabled}
|
||||||
|
showRightSidebar={form.values.layout.rightSidebarEnabled}
|
||||||
|
showPings={form.values.layout.pingsEnabled}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={12} sm={6}>
|
||||||
|
<Stack spacing="sm" h="100%" justify="space-between">
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<Checkbox
|
||||||
|
label={t('layout.enablelsidebar')}
|
||||||
|
description={t('layout.enablelsidebardesc')}
|
||||||
|
{...form.getInputProps('layout.leftSidebarEnabled', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t('layout.enablersidebar')}
|
||||||
|
description={t('layout.enablersidebardesc')}
|
||||||
|
{...form.getInputProps('layout.rightSidebarEnabled', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t('layout.enableping')}
|
||||||
|
{...form.getInputProps('layout.pingsEnabled', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
124
src/components/Board/Customize/Layout/LayoutPreview.tsx
Normal file
124
src/components/Board/Customize/Layout/LayoutPreview.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { Flex, Group, Indicator, Paper, Stack, createStyles } from '@mantine/core';
|
||||||
|
import { Logo } from '~/components/layout/Logo';
|
||||||
|
import { createDummyArray } from '~/tools/client/arrays';
|
||||||
|
|
||||||
|
type LayoutPreviewProps = {
|
||||||
|
showLeftSidebar: boolean;
|
||||||
|
showRightSidebar: boolean;
|
||||||
|
showPings: boolean;
|
||||||
|
};
|
||||||
|
export const LayoutPreview = ({
|
||||||
|
showLeftSidebar,
|
||||||
|
showRightSidebar,
|
||||||
|
showPings,
|
||||||
|
}: LayoutPreviewProps) => {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<Paper px="xs" py={4} withBorder>
|
||||||
|
<Group position="apart">
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Logo size="xs" />
|
||||||
|
</div>
|
||||||
|
<BaseElement width={60} height={10} />
|
||||||
|
<Group style={{ flex: 1 }} position="right">
|
||||||
|
<BaseElement width={10} height={10} />
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Flex gap={6}>
|
||||||
|
{showLeftSidebar && (
|
||||||
|
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
||||||
|
<Flex gap={5} wrap="wrap" align="end" w={65}>
|
||||||
|
{createDummyArray(5).map((_item, index) => (
|
||||||
|
<PlaceholderElement
|
||||||
|
height={index % 4 === 0 ? 60 + 5 : 30}
|
||||||
|
width={30}
|
||||||
|
key={`example-item-right-sidebard-${index}`}
|
||||||
|
index={index}
|
||||||
|
showPing={showPings}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Paper className={classes.primaryWrapper} p="xs" withBorder>
|
||||||
|
<Flex gap={5} wrap="wrap">
|
||||||
|
{createDummyArray(10).map((_item, index) => (
|
||||||
|
<PlaceholderElement
|
||||||
|
height={30}
|
||||||
|
width={index % 5 === 0 ? 60 + 5 : 30}
|
||||||
|
key={`example-item-main-${index}`}
|
||||||
|
index={index}
|
||||||
|
showPing={showPings}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{showRightSidebar && (
|
||||||
|
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
||||||
|
<Flex gap={5} align="start" wrap="wrap" w={65}>
|
||||||
|
{createDummyArray(5).map((_item, index) => (
|
||||||
|
<PlaceholderElement
|
||||||
|
height={30}
|
||||||
|
width={index % 4 === 0 ? 60 + 5 : 30}
|
||||||
|
key={`example-item-right-sidebard-${index}`}
|
||||||
|
index={index}
|
||||||
|
showPing={showPings}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
primaryWrapper: {
|
||||||
|
flexGrow: 2,
|
||||||
|
},
|
||||||
|
secondaryWrapper: {
|
||||||
|
flexGrow: 1,
|
||||||
|
maxWidth: 100,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const BaseElement = ({ height, width }: { height: number; width: number }) => (
|
||||||
|
<Paper
|
||||||
|
sx={(theme) => ({
|
||||||
|
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.gray[8] : theme.colors.gray[1],
|
||||||
|
})}
|
||||||
|
h={height}
|
||||||
|
p={2}
|
||||||
|
w={width}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
type PlaceholderElementProps = {
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
showPing: boolean;
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
const PlaceholderElement = ({ height, width, showPing, index }: PlaceholderElementProps) => {
|
||||||
|
if (showPing) {
|
||||||
|
return (
|
||||||
|
<Indicator
|
||||||
|
position="bottom-end"
|
||||||
|
size={5}
|
||||||
|
offset={10}
|
||||||
|
color={index % 4 === 0 ? 'red' : 'green'}
|
||||||
|
>
|
||||||
|
<BaseElement width={width} height={height} />
|
||||||
|
</Indicator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <BaseElement width={width} height={height} />;
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Stack, TextInput } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
|
import { useBoardCustomizationFormContext } from '../form';
|
||||||
|
|
||||||
|
export const PageMetadataCustomization = () => {
|
||||||
|
const { t } = useTranslation('settings/customization/page-appearance');
|
||||||
|
const form = useBoardCustomizationFormContext();
|
||||||
|
return (
|
||||||
|
<Stack spacing="sm">
|
||||||
|
<TextInput
|
||||||
|
label={t('pageTitle.label')}
|
||||||
|
description={t('pageTitle.description')}
|
||||||
|
placeholder="homarr"
|
||||||
|
{...form.getInputProps('pageMetadata.pageTitle')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={t('metaTitle.label')}
|
||||||
|
description={t('metaTitle.description')}
|
||||||
|
placeholder="homarr - the best dashboard"
|
||||||
|
{...form.getInputProps('pageMetadata.metaTitle')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={t('logo.label')}
|
||||||
|
description={t('logo.description')}
|
||||||
|
placeholder="/imgs/logo/logo.png"
|
||||||
|
{...form.getInputProps('pageMetadata.logoSrc')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={t('favicon.label')}
|
||||||
|
description={t('favicon.description')}
|
||||||
|
placeholder="/imgs/favicon/favicon.svg"
|
||||||
|
{...form.getInputProps('pageMetadata.faviconSrc')}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
43
src/components/Board/Customize/form.ts
Normal file
43
src/components/Board/Customize/form.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { DEFAULT_THEME, MANTINE_COLORS, MantineColor } from '@mantine/core';
|
||||||
|
import { createFormContext } from '@mantine/form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
layout: z.object({
|
||||||
|
leftSidebarEnabled: z.boolean(),
|
||||||
|
rightSidebarEnabled: z.boolean(),
|
||||||
|
pingsEnabled: z.boolean(),
|
||||||
|
}),
|
||||||
|
gridstack: z.object({
|
||||||
|
sm: z.number().min(1).max(8),
|
||||||
|
md: z.number().min(3).max(16),
|
||||||
|
lg: z.number().min(5).max(20),
|
||||||
|
}),
|
||||||
|
pageMetadata: z.object({
|
||||||
|
pageTitle: z.string(),
|
||||||
|
metaTitle: z.string(),
|
||||||
|
logoSrc: z.string(),
|
||||||
|
faviconSrc: z.string(),
|
||||||
|
}),
|
||||||
|
appearance: z.object({
|
||||||
|
backgroundSrc: z.string(),
|
||||||
|
primaryColor: z.custom<MantineColor>(
|
||||||
|
(value) => typeof value === 'string' && MANTINE_COLORS.includes(value)
|
||||||
|
),
|
||||||
|
secondaryColor: z.custom<MantineColor>(
|
||||||
|
(value) => typeof value === 'string' && MANTINE_COLORS.includes(value)
|
||||||
|
),
|
||||||
|
shade: z
|
||||||
|
.number()
|
||||||
|
.min(0)
|
||||||
|
.max(DEFAULT_THEME.colors['blue'].length - 1),
|
||||||
|
opacity: z.number().min(10).max(100),
|
||||||
|
customCss: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const [
|
||||||
|
BoardCustomizationFormProvider,
|
||||||
|
useBoardCustomizationFormContext,
|
||||||
|
useBoardCustomizationForm,
|
||||||
|
] = createFormContext<z.infer<typeof schema>>();
|
||||||
135
src/pages/board/[slug]/customize.tsx
Normal file
135
src/pages/board/[slug]/customize.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { Button, Container, Group, Paper, Stack, Text, Title } from '@mantine/core';
|
||||||
|
import {
|
||||||
|
IconArrowLeft,
|
||||||
|
IconBrush,
|
||||||
|
IconChartCandle,
|
||||||
|
IconDragDrop,
|
||||||
|
IconLayout,
|
||||||
|
TablerIconsProps,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import { GetServerSideProps } from 'next';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
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 { MainLayout } from '~/components/layout/main';
|
||||||
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||||
|
import { dashboardNamespaces } from '~/tools/server/translation-namespaces';
|
||||||
|
|
||||||
|
export default function CustomizationPage() {
|
||||||
|
const query = useRouter().query as { slug: string };
|
||||||
|
const { t } = useTranslation([
|
||||||
|
'settings/customization/general',
|
||||||
|
'settings/customization/color-selector',
|
||||||
|
]);
|
||||||
|
const form = useBoardCustomizationForm({
|
||||||
|
initialValues: {
|
||||||
|
layout: {
|
||||||
|
leftSidebarEnabled: false,
|
||||||
|
rightSidebarEnabled: false,
|
||||||
|
pingsEnabled: false,
|
||||||
|
},
|
||||||
|
appearance: {
|
||||||
|
backgroundSrc: '',
|
||||||
|
primaryColor: 'red',
|
||||||
|
secondaryColor: 'orange',
|
||||||
|
shade: 8,
|
||||||
|
opacity: 50,
|
||||||
|
customCss: '',
|
||||||
|
},
|
||||||
|
gridstack: {
|
||||||
|
sm: 3,
|
||||||
|
md: 6,
|
||||||
|
lg: 12,
|
||||||
|
},
|
||||||
|
pageMetadata: {
|
||||||
|
pageTitle: '',
|
||||||
|
metaTitle: '',
|
||||||
|
logoSrc: '',
|
||||||
|
faviconSrc: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<Container>
|
||||||
|
<Paper p="xl" py="sm" mih="100%" withBorder>
|
||||||
|
<Stack>
|
||||||
|
<Group position="apart">
|
||||||
|
<Title order={2}>Customization for {query.slug} Board</Title>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
href={`/board/${query.slug}`}
|
||||||
|
variant="light"
|
||||||
|
leftIcon={<IconArrowLeft size={16} />}
|
||||||
|
>
|
||||||
|
Back to Board
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<BoardCustomizationFormProvider form={form}>
|
||||||
|
<form>
|
||||||
|
<Stack spacing="xl">
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<SectionTitle type="layout" icon={IconLayout} />
|
||||||
|
<LayoutCustomization />
|
||||||
|
</Stack>
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<SectionTitle type="gridstack" icon={IconDragDrop} />
|
||||||
|
<GridstackCustomization />
|
||||||
|
</Stack>
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<SectionTitle type="pageMetadata" icon={IconChartCandle} />
|
||||||
|
<PageMetadataCustomization />
|
||||||
|
</Stack>
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<SectionTitle type="appereance" icon={IconBrush} />
|
||||||
|
<AppearanceCustomization />
|
||||||
|
</Stack>
|
||||||
|
<Button type="submit">Save changes</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</BoardCustomizationFormProvider>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SectionTitleProps = {
|
||||||
|
type: 'layout' | 'gridstack' | 'pageMetadata' | 'appereance';
|
||||||
|
icon: (props: TablerIconsProps) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SectionTitle = ({ type, icon: Icon }: SectionTitleProps) => {
|
||||||
|
const { t } = useTranslation('settings/customization/general');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={0}>
|
||||||
|
<Group spacing="xs">
|
||||||
|
<Icon size={16} />
|
||||||
|
<Title order={5}>{t(`accordeon.${type}.name`)}</Title>
|
||||||
|
</Group>
|
||||||
|
<Text color="dimmed">{t(`accordeon.${type}.description`)}</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps = async ({ req, res, locale }) => {
|
||||||
|
const translations = await getServerSideTranslations(dashboardNamespaces, locale, req, res);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
...translations,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -63,9 +63,9 @@ export default function HomePage({ config: initialConfig }: DashboardServerSideP
|
|||||||
useInitConfig(initialConfig);
|
useInitConfig(initialConfig);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<Layout>
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
<LoadConfigComponent />
|
<LoadConfigComponent />
|
||||||
</MainLayout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user