Add new config format

This commit is contained in:
Meierschlumpf
2022-12-04 17:36:30 +01:00
parent b2f5149527
commit d5a3b3f3ba
76 changed files with 2461 additions and 1034 deletions

View File

@@ -0,0 +1,43 @@
import { TextInput } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { ChangeEventHandler, useState } from 'react';
import { useConfigContext } from '../../../config/provider';
import { useConfigStore } from '../../../config/store';
interface BackgroundChangerProps {
defaultValue: string | undefined;
}
export const BackgroundChanger = ({ defaultValue }: BackgroundChangerProps) => {
const { t } = useTranslation('settings/customization/page-appearance');
const updateConfig = useConfigStore((x) => x.updateConfig);
const { name: configName } = useConfigContext();
const [backgroundImageUrl, setBackgroundImageUrl] = useState(defaultValue);
if (!configName) return null;
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
const value = ev.currentTarget.value;
const backgroundImageUrl = value.trim().length === 0 ? undefined : value;
setBackgroundImageUrl(backgroundImageUrl);
updateConfig(configName, (prev) => ({
...prev,
settings: {
...prev.settings,
customization: {
...prev.settings.customization,
backgroundImageUrl,
},
},
}));
};
return (
<TextInput
label={t('background.label')}
placeholder="/imgs/background.png"
value={backgroundImageUrl}
onChange={handleChange}
/>
);
};

View File

@@ -0,0 +1,104 @@
import React, { useState } from 'react';
import {
ColorSwatch,
Grid,
Group,
MantineTheme,
Popover,
Text,
useMantineTheme,
} from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { useColorTheme } from '../../../tools/color';
import { useDisclosure } from '@mantine/hooks';
import { useConfigStore } from '../../../config/store';
import { useConfigContext } from '../../../config/provider';
interface ColorControlProps {
defaultValue: MantineTheme['primaryColor'] | undefined;
type: 'primary' | 'secondary';
}
export function ColorSelector({ type, defaultValue }: ColorControlProps) {
const { t } = useTranslation('settings/customization/color-selector');
const [color, setColor] = useState(defaultValue);
const [popoverOpened, popover] = useDisclosure(false);
const { setPrimaryColor, setSecondaryColor } = useColorTheme();
const { name: configName } = useConfigContext();
const updateConfig = useConfigStore((x) => x.updateConfig);
const theme = useMantineTheme();
const colors = Object.keys(theme.colors).map((color) => ({
swatch: theme.colors[color][6],
color,
}));
if (!color || !configName) return null;
const handleSelection = (color: MantineTheme['primaryColor']) => {
setColor(color);
if (type === 'primary') setPrimaryColor(color);
else setSecondaryColor(color);
updateConfig(configName, (prev) => {
const colors = prev.settings.customization.colors;
colors[type] = color;
return {
...prev,
settings: {
...prev.settings,
customization: {
...prev.settings.customization,
colors,
},
},
};
});
};
const swatches = colors.map(({ color, swatch }) => (
<Grid.Col span={2} key={color}>
<ColorSwatch
component="button"
type="button"
onClick={() => handleSelection(color)}
color={swatch}
size={22}
style={{ cursor: 'pointer' }}
/>
</Grid.Col>
));
return (
<Group>
<Popover
width={250}
withinPortal
opened={popoverOpened}
onClose={popover.close}
position="left"
withArrow
>
<Popover.Target>
<ColorSwatch
component="button"
type="button"
color={theme.colors[color][6]}
onClick={popover.toggle}
size={22}
style={{ cursor: 'pointer' }}
/>
</Popover.Target>
<Popover.Dropdown>
<Grid gutter="lg" columns={14}>
{swatches}
</Grid>
</Popover.Dropdown>
</Popover>
<Text>
{t('suffix', {
color: type[0].toUpperCase() + type.slice(1),
})}
</Text>
</Group>
);
}

View File

@@ -0,0 +1,44 @@
import { Textarea } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { ChangeEventHandler, useState } from 'react';
import { useConfigContext } from '../../../config/provider';
import { useConfigStore } from '../../../config/store';
interface CustomCssChangerProps {
defaultValue: string | undefined;
}
export const CustomCssChanger = ({ defaultValue }: CustomCssChangerProps) => {
const { t } = useTranslation('settings/customization/page-appearance');
const updateConfig = useConfigStore((x) => x.updateConfig);
const { name: configName } = useConfigContext();
const [customCss, setCustomCss] = useState(defaultValue);
if (!configName) return null;
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = (ev) => {
const value = ev.currentTarget.value;
const customCss = value.trim().length === 0 ? undefined : value;
setCustomCss(customCss);
updateConfig(configName, (prev) => ({
...prev,
settings: {
...prev.settings,
customization: {
...prev.settings.customization,
customCss,
},
},
}));
};
return (
<Textarea
minRows={5}
label={t('customCSS.label')}
placeholder={t('customCSS.placeholder')}
value={customCss}
onChange={handleChange}
/>
);
};

View File

@@ -0,0 +1,43 @@
import { TextInput } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { ChangeEventHandler, useState } from 'react';
import { useConfigContext } from '../../../config/provider';
import { useConfigStore } from '../../../config/store';
interface FaviconChangerProps {
defaultValue: string | undefined;
}
export const FaviconChanger = ({ defaultValue }: FaviconChangerProps) => {
const { t } = useTranslation('settings/customization/page-appearance');
const updateConfig = useConfigStore((x) => x.updateConfig);
const { name: configName } = useConfigContext();
const [faviconUrl, setFaviconUrl] = useState(defaultValue);
if (!configName) return null;
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
const value = ev.currentTarget.value;
const faviconUrl = value.trim().length === 0 ? undefined : value;
setFaviconUrl(faviconUrl);
updateConfig(configName, (prev) => ({
...prev,
settings: {
...prev.settings,
customization: {
...prev.settings.customization,
faviconUrl,
},
},
}));
};
return (
<TextInput
label={t('favicon.label')}
placeholder="/imgs/favicon/favicon.svg"
value={faviconUrl}
onChange={handleChange}
/>
);
};

View File

@@ -0,0 +1,164 @@
import {
Box,
Center,
Checkbox,
createStyles,
Group,
Paper,
Stack,
Text,
Title,
} from '@mantine/core';
import { IconBrandDocker, IconLayout, IconSearch } from '@tabler/icons';
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
import { useConfigContext } from '../../../config/provider';
import { useConfigStore } from '../../../config/store';
import { CustomizationSettingsType } from '../../../types/settings';
import { Logo } from '../../layout/Logo';
interface LayoutSelectorProps {
defaultLayout: CustomizationSettingsType['layout'] | undefined;
}
// TODO: add translations
export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
const { classes } = useStyles();
const { name: configName } = useConfigContext();
const updateConfig = useConfigStore((x) => x.updateConfig);
const [leftSidebar, setLeftSidebar] = useState(defaultLayout?.enabledLeftSidebar ?? true);
const [rightSidebar, setRightSidebar] = useState(defaultLayout?.enabledRightSidebar ?? true);
const [docker, setDocker] = useState(defaultLayout?.enabledDocker ?? false);
const [ping, setPing] = useState(defaultLayout?.enabledPing ?? false);
const [searchBar, setSearchBar] = useState(defaultLayout?.enabledSearchbar ?? false);
if (!configName) return null;
const handleChange = (
key: keyof CustomizationSettingsType['layout'],
event: ChangeEvent<HTMLInputElement>,
setState: Dispatch<SetStateAction<boolean>>
) => {
const value = event.target.checked;
setState(value);
updateConfig(configName, (prev) => {
const layout = prev.settings.customization.layout;
layout[key] = value;
return {
...prev,
settings: {
...prev.settings,
customization: {
...prev.settings.customization,
layout,
},
},
};
});
};
return (
<Box className={classes.box} p="xl" pb="sm">
<Stack spacing="xs">
<Group spacing={5}>
<IconLayout size={20} />
<Title order={6}>Dashboard layout</Title>
</Group>
<Text color="dimmed" size="sm">
You can adjust the layout of the Dashboard to your preferences. The main are cannot be
turned on or off
</Text>
<Paper px="xs" py={2} withBorder>
<Group position="apart">
<Logo size="xs" />
<Group spacing={4}>
{searchBar ? (
<Paper withBorder p={2} w={60}>
<Group spacing={2} align="center">
<IconSearch size={8} />
<Text size={8} color="dimmed">
Search
</Text>
</Group>
</Paper>
) : null}
{docker ? <IconBrandDocker size={18} color="#0db7ed" /> : null}
</Group>
</Group>
</Paper>
<Group align="stretch">
{leftSidebar && (
<Paper p="xs" withBorder>
<Center style={{ height: '100%' }}>
<Text align="center">Sidebar</Text>
</Center>
</Paper>
)}
<Paper className={classes.main} p="xs" withBorder>
<Text align="center">Main</Text>
<Text color="dimmed" size="xs" align="center">
Can be used for categories,
<br />
services and integrations
</Text>
</Paper>
{rightSidebar && (
<Paper p="xs" withBorder>
<Center style={{ height: '100%' }}>
<Text align="center">Sidebar</Text>
</Center>
</Paper>
)}
</Group>
<Stack spacing="xs">
<Checkbox
label="Enable left sidebar"
description="Optional. Can be used for services and integrations only"
checked={leftSidebar}
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
/>
<Checkbox
label="Enable right sidebar"
description="Optional. Can be used for services and integrations only"
checked={rightSidebar}
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
/>
<Checkbox
label="Enable search bar"
checked={searchBar}
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
/>
<Checkbox
label="Enable docker"
checked={docker}
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
/>
<Checkbox
label="Enable pings"
checked={ping}
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
/>
</Stack>
</Stack>
</Box>
);
};
const useStyles = createStyles((theme) => ({
main: {
flexGrow: 1,
},
box: {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1],
borderRadius: theme.radius.md,
},
}));

View File

@@ -0,0 +1,43 @@
import { TextInput } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { ChangeEventHandler, useState } from 'react';
import { useConfigContext } from '../../../config/provider';
import { useConfigStore } from '../../../config/store';
interface LogoImageChangerProps {
defaultValue: string | undefined;
}
export const LogoImageChanger = ({ defaultValue }: LogoImageChangerProps) => {
const { t } = useTranslation('settings/customization/page-appearance');
const updateConfig = useConfigStore((x) => x.updateConfig);
const { name: configName } = useConfigContext();
const [logoImageSrc, setLogoImageSrc] = useState(defaultValue);
if (!configName) return null;
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
const value = ev.currentTarget.value;
const logoImageSrc = value.trim().length === 0 ? undefined : value;
setLogoImageSrc(logoImageSrc);
updateConfig(configName, (prev) => ({
...prev,
settings: {
...prev.settings,
customization: {
...prev.settings.customization,
logoImageUrl: logoImageSrc,
},
},
}));
};
return (
<TextInput
label={t('logo.label')}
placeholder="/imgs/logo/logo.png"
value={logoImageSrc}
onChange={handleChange}
/>
);
};

View File

@@ -0,0 +1,43 @@
import { TextInput } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { ChangeEventHandler, useState } from 'react';
import { useConfigContext } from '../../../config/provider';
import { useConfigStore } from '../../../config/store';
interface MetaTitleChangerProps {
defaultValue: string | undefined;
}
export const MetaTitleChanger = ({ defaultValue }: MetaTitleChangerProps) => {
const { t } = useTranslation('settings/customization/page-appearance');
const updateConfig = useConfigStore((x) => x.updateConfig);
const { name: configName } = useConfigContext();
const [metaTitle, setMetaTitle] = useState(defaultValue);
if (!configName) return null;
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
const value = ev.currentTarget.value;
const metaTitle = value.trim().length === 0 ? undefined : value;
setMetaTitle(metaTitle);
updateConfig(configName, (prev) => ({
...prev,
settings: {
...prev.settings,
customization: {
...prev.settings.customization,
metaTitle,
},
},
}));
};
return (
<TextInput
label={t('metaTitle.label')}
placeholder={t('metaTitle.placeholder')}
value={metaTitle}
onChange={handleChange}
/>
);
};

View File

@@ -0,0 +1,60 @@
import React, { useState } from 'react';
import { Text, Slider, Stack } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { useConfigContext } from '../../../config/provider';
import { useConfigStore } from '../../../config/store';
interface OpacitySelectorProps {
defaultValue: number | undefined;
}
export function OpacitySelector({ defaultValue }: OpacitySelectorProps) {
const [opacity, setOpacity] = useState(defaultValue || 100);
const { t } = useTranslation('settings/customization/opacity-selector');
const { name: configName } = useConfigContext();
const updateConfig = useConfigStore((x) => x.updateConfig);
if (!configName) return null;
const handleChange = (opacity: number) => {
setOpacity(opacity);
updateConfig(configName, (prev) => ({
...prev,
settings: {
...prev.settings,
customization: {
...prev.settings.customization,
appOpacity: opacity,
},
},
}));
};
return (
<Stack spacing="xs">
<Text>{t('label')}</Text>
<Slider
defaultValue={opacity}
step={10}
min={10}
marks={MARKS}
styles={{ markLabel: { fontSize: 'xx-small' } }}
onChange={handleChange}
/>
</Stack>
);
}
const MARKS = [
{ 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' },
];

View File

@@ -0,0 +1,43 @@
import { TextInput } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { ChangeEventHandler, useState } from 'react';
import { useConfigContext } from '../../../config/provider';
import { useConfigStore } from '../../../config/store';
interface PageTitleChangerProps {
defaultValue: string | undefined;
}
export const PageTitleChanger = ({ defaultValue }: PageTitleChangerProps) => {
const { t } = useTranslation('settings/customization/page-appearance');
const updateConfig = useConfigStore((x) => x.updateConfig);
const { name: configName } = useConfigContext();
const [pageTitle, setPageTitle] = useState(defaultValue);
if (!configName) return null;
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
const value = ev.currentTarget.value;
const pageTitle = value.trim().length === 0 ? undefined : value;
setPageTitle(pageTitle);
updateConfig(configName, (prev) => ({
...prev,
settings: {
...prev.settings,
customization: {
...prev.settings.customization,
pageTitle,
},
},
}));
};
return (
<TextInput
label={t('pageTitle.label')}
placeholder={t('pageTitle.placeholder')}
value={pageTitle}
onChange={handleChange}
/>
);
};

View File

@@ -0,0 +1,101 @@
import React, { useState } from 'react';
import {
ColorSwatch,
Group,
Popover,
Text,
useMantineTheme,
MantineTheme,
Stack,
Grid,
} from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { useColorTheme } from '../../../tools/color';
import { useDisclosure } from '@mantine/hooks';
import { useConfigStore } from '../../../config/store';
import { useConfigContext } from '../../../config/provider';
interface ShadeSelectorProps {
defaultValue: MantineTheme['primaryShade'] | undefined;
}
export function ShadeSelector({ defaultValue }: ShadeSelectorProps) {
const { t } = useTranslation('settings/customization/shade-selector');
const [shade, setShade] = useState(defaultValue);
const [popoverOpened, popover] = useDisclosure(false);
const { primaryColor, setPrimaryShade } = useColorTheme();
const { name: configName } = useConfigContext();
const updateConfig = useConfigStore((x) => x.updateConfig);
const theme = useMantineTheme();
const primaryShades = theme.colors[primaryColor].map((s, i) => ({
swatch: theme.colors[primaryColor][i],
shade: i as MantineTheme['primaryShade'],
}));
if (shade === undefined || !configName) return null;
const handleSelection = (shade: MantineTheme['primaryShade']) => {
setPrimaryShade(shade);
setShade(shade);
updateConfig(configName, (prev) => ({
...prev,
settings: {
...prev.settings,
customization: {
...prev.settings.customization,
colors: {
...prev.settings.customization.colors,
shade,
},
},
},
}));
};
const primarySwatches = primaryShades.map(({ swatch, shade }) => (
<Grid.Col span={1} key={Number(shade)}>
<ColorSwatch
component="button"
type="button"
onClick={() => handleSelection(shade)}
color={swatch}
size={22}
style={{ cursor: 'pointer' }}
/>
</Grid.Col>
));
return (
<Group>
<Popover
width={350}
withinPortal
opened={popoverOpened}
onClose={popover.close}
position="left"
withArrow
>
<Popover.Target>
<ColorSwatch
component="button"
type="button"
color={theme.colors[primaryColor][Number(shade)]}
onClick={popover.toggle}
size={22}
style={{ display: 'block', cursor: 'pointer' }}
/>
</Popover.Target>
<Popover.Dropdown>
<Stack spacing="xs">
<Grid gutter="lg" columns={10}>
{primarySwatches}
</Grid>
</Stack>
</Popover.Dropdown>
</Popover>
<Text>{t('label')}</Text>
</Group>
);
}