mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 17:26:26 +01:00
♻️ Improved code structure for layout, remove most settings components
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Flex, Group, Indicator, Paper, Stack, createStyles } from '@mantine/core';
|
import { Flex, Group, Indicator, Paper, Stack, createStyles } from '@mantine/core';
|
||||||
import { Logo } from '~/components/layout/Logo';
|
import { Logo } from '~/components/layout/Common/Logo';
|
||||||
import { createDummyArray } from '~/tools/client/arrays';
|
import { createDummyArray } from '~/tools/client/arrays';
|
||||||
|
|
||||||
type LayoutPreviewProps = {
|
type LayoutPreviewProps = {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { motion } from 'framer-motion';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { AppType } from '../../../../types/app';
|
import { AppType } from '../../../../types/app';
|
||||||
import { useCardStyles } from '../../../layout/useCardStyles';
|
import { useCardStyles } from '../../../layout/Common/useCardStyles';
|
||||||
import { useEditModeStore } from '../../Views/useEditModeStore';
|
import { useEditModeStore } from '../../Views/useEditModeStore';
|
||||||
import { HomarrCardWrapper } from '../HomarrCardWrapper';
|
import { HomarrCardWrapper } from '../HomarrCardWrapper';
|
||||||
import { BaseTileProps } from '../type';
|
import { BaseTileProps } from '../type';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Card, CardProps } from '@mantine/core';
|
import { Card, CardProps } from '@mantine/core';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import { useCardStyles } from '../../layout/useCardStyles';
|
import { useCardStyles } from '../../layout/Common/useCardStyles';
|
||||||
import { useEditModeStore } from '../Views/useEditModeStore';
|
import { useEditModeStore } from '../Views/useEditModeStore';
|
||||||
|
|
||||||
interface HomarrCardWrapperProps extends CardProps {
|
interface HomarrCardWrapperProps extends CardProps {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { useTranslation } from 'next-i18next';
|
|||||||
|
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { CategoryType } from '../../../../types/category';
|
import { CategoryType } from '../../../../types/category';
|
||||||
import { useCardStyles } from '../../../layout/useCardStyles';
|
import { useCardStyles } from '../../../layout/Common/useCardStyles';
|
||||||
import { useEditModeStore } from '../../Views/useEditModeStore';
|
import { useEditModeStore } from '../../Views/useEditModeStore';
|
||||||
import { WrapperContent } from '../WrapperContent';
|
import { WrapperContent } from '../WrapperContent';
|
||||||
import { useGridstack } from '../gridstack/use-gridstack';
|
import { useGridstack } from '../gridstack/use-gridstack';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Card } from '@mantine/core';
|
import { Card } from '@mantine/core';
|
||||||
import { RefObject } from 'react';
|
import { RefObject } from 'react';
|
||||||
|
|
||||||
import { useCardStyles } from '../../../layout/useCardStyles';
|
import { useCardStyles } from '../../../layout/Common/useCardStyles';
|
||||||
import { WrapperContent } from '../WrapperContent';
|
import { WrapperContent } from '../WrapperContent';
|
||||||
import { useGridstack } from '../gridstack/use-gridstack';
|
import { useGridstack } from '../gridstack/use-gridstack';
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { ScrollArea, Space, Stack, Text } from '@mantine/core';
|
|
||||||
import { useViewportSize } from '@mantine/hooks';
|
|
||||||
|
|
||||||
import { useConfigContext } from '../../../config/provider';
|
|
||||||
import ConfigChanger from '../../Config/ConfigChanger';
|
|
||||||
import ConfigActions from './Config/ConfigActions';
|
|
||||||
import LanguageSelect from './Language/LanguageSelect';
|
|
||||||
import { SearchEngineSelector } from './SearchEngine/SearchEngineSelector';
|
|
||||||
|
|
||||||
export default function CommonSettings() {
|
|
||||||
const { config } = useConfigContext();
|
|
||||||
const { height, width } = useViewportSize();
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
return (
|
|
||||||
<Text color="red" align="center">
|
|
||||||
No active config
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ScrollArea style={{ height: height - 100 }} scrollbarSize={5}>
|
|
||||||
<Stack>
|
|
||||||
<SearchEngineSelector searchEngine={config.settings.common.searchEngine} />
|
|
||||||
<Space />
|
|
||||||
<LanguageSelect />
|
|
||||||
<ConfigChanger />
|
|
||||||
<ConfigActions />
|
|
||||||
</Stack>
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
Alert,
|
|
||||||
Center,
|
|
||||||
Flex,
|
|
||||||
Text,
|
|
||||||
createStyles,
|
|
||||||
useMantineTheme,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
|
||||||
import { openConfirmModal } from '@mantine/modals';
|
|
||||||
import { showNotification } from '@mantine/notifications';
|
|
||||||
import {
|
|
||||||
IconAlertTriangle,
|
|
||||||
IconCheck,
|
|
||||||
IconCopy,
|
|
||||||
IconDownload,
|
|
||||||
IconTrash,
|
|
||||||
IconX,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import fileDownload from 'js-file-download';
|
|
||||||
import { Trans, useTranslation } from 'next-i18next';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { api } from '~/utils/api';
|
|
||||||
|
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
|
||||||
import { useConfigStore } from '../../../../config/store';
|
|
||||||
import Tip from '../../../layout/Tip';
|
|
||||||
import { CreateConfigCopyModal } from './CreateCopyModal';
|
|
||||||
|
|
||||||
export default function ConfigActions() {
|
|
||||||
const { t } = useTranslation(['settings/general/config-changer', 'settings/common', 'common']);
|
|
||||||
const [createCopyModalOpened, createCopyModal] = useDisclosure(false);
|
|
||||||
const { config } = useConfigContext();
|
|
||||||
const { mutateAsync } = useDeleteConfigMutation();
|
|
||||||
|
|
||||||
if (!config) return null;
|
|
||||||
|
|
||||||
const handleDownload = () => {
|
|
||||||
fileDownload(JSON.stringify(config, null, '\t'), `${config?.configProperties.name}.json`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeletion = async () => {
|
|
||||||
openConfirmModal({
|
|
||||||
title: t('modal.confirmDeletion.title'),
|
|
||||||
children: (
|
|
||||||
<>
|
|
||||||
<Alert icon={<IconAlertTriangle />} mb="md">
|
|
||||||
<Trans
|
|
||||||
i18nKey="settings/general/config-changer:modal.confirmDeletion.warningText"
|
|
||||||
values={{ configName: config.configProperties.name ?? 'default' }}
|
|
||||||
components={{ b: <b />, code: <code /> }}
|
|
||||||
/>
|
|
||||||
</Alert>
|
|
||||||
<Text>{t('modal.confirmDeletion.text')}</Text>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
labels: {
|
|
||||||
confirm: (
|
|
||||||
<Trans
|
|
||||||
i18nKey="settings/general/config-changer:modal.confirmDeletion.buttons.confirm"
|
|
||||||
values={{ configName: config.configProperties.name ?? 'default' }}
|
|
||||||
components={{ b: <b />, code: <code /> }}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cancel: t('common:cancel'),
|
|
||||||
},
|
|
||||||
zIndex: 201,
|
|
||||||
onConfirm: async () => {
|
|
||||||
const response = await mutateAsync({
|
|
||||||
name: config?.configProperties.name ?? 'default',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const { classes } = useStyles();
|
|
||||||
const { colors } = useMantineTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CreateConfigCopyModal
|
|
||||||
opened={createCopyModalOpened}
|
|
||||||
closeModal={createCopyModal.close}
|
|
||||||
initialConfigName={config.configProperties.name}
|
|
||||||
/>
|
|
||||||
<Flex gap="xs" mt="xs" justify="stretch">
|
|
||||||
<ActionIcon className={classes.actionIcon} onClick={handleDownload} variant="default">
|
|
||||||
<IconDownload size={20} />
|
|
||||||
<Text size="sm">{t('buttons.download')}</Text>
|
|
||||||
</ActionIcon>
|
|
||||||
<ActionIcon
|
|
||||||
className={classes.actionIcon}
|
|
||||||
onClick={handleDeletion}
|
|
||||||
color="red"
|
|
||||||
variant="light"
|
|
||||||
>
|
|
||||||
<IconTrash color={colors.red[2]} size={20} />
|
|
||||||
<Text size="sm">{t('buttons.delete.text')}</Text>
|
|
||||||
</ActionIcon>
|
|
||||||
<ActionIcon className={classes.actionIcon} onClick={createCopyModal.open} variant="default">
|
|
||||||
<IconCopy size={20} />
|
|
||||||
<Text size="sm">{t('buttons.saveCopy')}</Text>
|
|
||||||
</ActionIcon>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Center>
|
|
||||||
<Tip>{t('settings/common:tips.configTip')}</Tip>
|
|
||||||
</Center>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const useDeleteConfigMutation = () => {
|
|
||||||
const { t } = useTranslation(['settings/general/config-changer']);
|
|
||||||
const router = useRouter();
|
|
||||||
const { removeConfig } = useConfigStore();
|
|
||||||
|
|
||||||
return api.config.delete.useMutation({
|
|
||||||
onError(error) {
|
|
||||||
if (error.data?.code === 'FORBIDDEN') {
|
|
||||||
showNotification({
|
|
||||||
title: t('buttons.delete.notifications.deleteFailedDefaultConfig.title'),
|
|
||||||
icon: <IconX />,
|
|
||||||
color: 'red',
|
|
||||||
autoClose: 1500,
|
|
||||||
radius: 'md',
|
|
||||||
message: t('buttons.delete.notifications.deleteFailedDefaultConfig.message'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
showNotification({
|
|
||||||
title: t('buttons.delete.notifications.deleteFailed.title'),
|
|
||||||
icon: <IconX />,
|
|
||||||
color: 'red',
|
|
||||||
autoClose: 1500,
|
|
||||||
radius: 'md',
|
|
||||||
message: t('buttons.delete.notifications.deleteFailed.message'),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSuccess(data, variables) {
|
|
||||||
showNotification({
|
|
||||||
title: t('buttons.delete.notifications.deleted.title'),
|
|
||||||
icon: <IconCheck />,
|
|
||||||
color: 'green',
|
|
||||||
autoClose: 1500,
|
|
||||||
radius: 'md',
|
|
||||||
message: t('buttons.delete.notifications.deleted.message'),
|
|
||||||
});
|
|
||||||
|
|
||||||
removeConfig(variables.name);
|
|
||||||
|
|
||||||
router.push('/');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const useStyles = createStyles(() => ({
|
|
||||||
actionIcon: {
|
|
||||||
width: 'auto',
|
|
||||||
height: 'auto',
|
|
||||||
maxWidth: 'auto',
|
|
||||||
maxHeight: 'auto',
|
|
||||||
flexGrow: 1,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
textAlign: 'center',
|
|
||||||
rowGap: 10,
|
|
||||||
padding: 10,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import { Button, Group, Modal, TextInput, Title } from '@mantine/core';
|
|
||||||
import { useForm } from '@mantine/form';
|
|
||||||
import { showNotification } from '@mantine/notifications';
|
|
||||||
import { IconCheck, IconX } from '@tabler/icons-react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useConfigContext } from '~/config/provider';
|
|
||||||
import { api } from '~/utils/api';
|
|
||||||
|
|
||||||
import { useConfigStore } from '../../../../config/store';
|
|
||||||
|
|
||||||
interface CreateConfigCopyModalProps {
|
|
||||||
opened: boolean;
|
|
||||||
closeModal: () => void;
|
|
||||||
initialConfigName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CreateConfigCopyModal = ({
|
|
||||||
opened,
|
|
||||||
closeModal,
|
|
||||||
initialConfigName,
|
|
||||||
}: CreateConfigCopyModalProps) => {
|
|
||||||
const { configs } = useConfigStore();
|
|
||||||
const { config } = useConfigContext();
|
|
||||||
const { t } = useTranslation(['settings/general/config-changer']);
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
initialValues: {
|
|
||||||
configName: initialConfigName,
|
|
||||||
},
|
|
||||||
validate: {
|
|
||||||
configName: (value) => {
|
|
||||||
if (!value) {
|
|
||||||
return t('modal.copy.form.configName.validation.required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const configNames = configs.map((x) => x.value.configProperties.name);
|
|
||||||
if (configNames.includes(value)) {
|
|
||||||
return t('modal.copy.form.configName.validation.notUnique');
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
validateInputOnChange: true,
|
|
||||||
validateInputOnBlur: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync } = useCopyConfigMutation();
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
form.setFieldValue('configName', initialConfigName);
|
|
||||||
closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (values: typeof form.values) => {
|
|
||||||
if (!form.isValid) return;
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
throw new Error('config is not defiend');
|
|
||||||
}
|
|
||||||
|
|
||||||
const copiedConfig = config;
|
|
||||||
copiedConfig.configProperties.name = form.values.configName;
|
|
||||||
|
|
||||||
await mutateAsync({
|
|
||||||
name: form.values.configName,
|
|
||||||
config: copiedConfig,
|
|
||||||
});
|
|
||||||
closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
radius="md"
|
|
||||||
opened={opened}
|
|
||||||
onClose={handleClose}
|
|
||||||
title={<Title order={4}>{t('modal.copy.title')}</Title>}
|
|
||||||
>
|
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
|
||||||
<TextInput
|
|
||||||
label={t('modal.copy.form.configName.label')}
|
|
||||||
placeholder={t('modal.copy.form.configName.placeholder') ?? undefined}
|
|
||||||
{...form.getInputProps('configName')}
|
|
||||||
/>
|
|
||||||
<Group position="right" mt="md">
|
|
||||||
<Button type="submit" disabled={!form.isValid()}>
|
|
||||||
{t('modal.copy.form.submitButton')}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useCopyConfigMutation = () => {
|
|
||||||
const { t } = useTranslation(['settings/general/config-changer']);
|
|
||||||
const utils = api.useContext();
|
|
||||||
|
|
||||||
return api.config.save.useMutation({
|
|
||||||
onSuccess(_data, variables) {
|
|
||||||
showNotification({
|
|
||||||
title: t('modal.copy.events.configCopied.title'),
|
|
||||||
icon: <IconCheck />,
|
|
||||||
color: 'green',
|
|
||||||
autoClose: 1500,
|
|
||||||
radius: 'md',
|
|
||||||
message: t('modal.copy.events.configCopied.message', { configName: variables.name }),
|
|
||||||
});
|
|
||||||
// Invalidate a query to fetch new config
|
|
||||||
utils.config.all.invalidate();
|
|
||||||
},
|
|
||||||
onError(_error, variables) {
|
|
||||||
showNotification({
|
|
||||||
title: t('modal.events.configNotCopied.title'),
|
|
||||||
icon: <IconX />,
|
|
||||||
color: 'red',
|
|
||||||
autoClose: 1500,
|
|
||||||
radius: 'md',
|
|
||||||
message: t('modal.events.configNotCopied.message', { configName: variables.name }),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import { Accordion, Checkbox, Grid, Group, Stack, Text } from '@mantine/core';
|
|
||||||
import {
|
|
||||||
IconAccessible,
|
|
||||||
IconBrush,
|
|
||||||
IconChartCandle,
|
|
||||||
IconCode,
|
|
||||||
IconDragDrop,
|
|
||||||
IconLayout,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { i18n, useTranslation } from 'next-i18next';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { env } from '~/env';
|
|
||||||
|
|
||||||
import { AccessibilitySettings } from './Accessibility/AccessibilitySettings';
|
|
||||||
import { GridstackConfiguration } from './Layout/GridstackConfiguration';
|
|
||||||
import { LayoutSelector } from './Layout/LayoutSelector';
|
|
||||||
import { BackgroundChanger } from './Meta/BackgroundChanger';
|
|
||||||
import { FaviconChanger } from './Meta/FaviconChanger';
|
|
||||||
import { LogoImageChanger } from './Meta/LogoImageChanger';
|
|
||||||
import { BrowserTabTitle } from './Meta/MetaTitleChanger';
|
|
||||||
import { DashboardTitleChanger } from './Meta/PageTitleChanger';
|
|
||||||
import { ColorSelector } from './Theme/ColorSelector';
|
|
||||||
import { CustomCssChanger } from './Theme/CustomCssChanger';
|
|
||||||
import { DashboardTilesOpacitySelector } from './Theme/OpacitySelector';
|
|
||||||
import { ShadeSelector } from './Theme/ShadeSelector';
|
|
||||||
|
|
||||||
export const CustomizationSettingsAccordeon = () => {
|
|
||||||
const items = getItems().map((item) => (
|
|
||||||
<Accordion.Item value={item.id} key={item.label}>
|
|
||||||
<Accordion.Control>
|
|
||||||
<AccordionLabel {...item} />
|
|
||||||
</Accordion.Control>
|
|
||||||
<Accordion.Panel>
|
|
||||||
<Text size="sm">{item.content}</Text>
|
|
||||||
</Accordion.Panel>
|
|
||||||
</Accordion.Item>
|
|
||||||
));
|
|
||||||
return (
|
|
||||||
<Accordion variant="contained" chevronPosition="right">
|
|
||||||
{items}
|
|
||||||
</Accordion>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface AccordionLabelProps {
|
|
||||||
label: string;
|
|
||||||
image: ReactNode;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AccordionLabel = ({ label, image, description }: AccordionLabelProps) => (
|
|
||||||
<Group noWrap>
|
|
||||||
{image}
|
|
||||||
<div>
|
|
||||||
<Text>{label}</Text>
|
|
||||||
<Text size="sm" color="dimmed" weight={400}>
|
|
||||||
{description}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
|
|
||||||
const getItems = () => {
|
|
||||||
const { t } = useTranslation([
|
|
||||||
'settings/customization/general',
|
|
||||||
'settings/customization/color-selector',
|
|
||||||
]);
|
|
||||||
const items = [
|
|
||||||
{
|
|
||||||
id: 'layout',
|
|
||||||
image: <IconLayout />,
|
|
||||||
label: t('accordeon.layout.name'),
|
|
||||||
description: t('accordeon.layout.description'),
|
|
||||||
content: <LayoutSelector />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gridstack',
|
|
||||||
image: <IconDragDrop />,
|
|
||||||
label: t('accordeon.gridstack.name'),
|
|
||||||
description: t('accordeon.gridstack.description'),
|
|
||||||
content: <GridstackConfiguration />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'accessibility',
|
|
||||||
image: <IconAccessible />,
|
|
||||||
label: t('accordeon.accessibility.name'),
|
|
||||||
description: t('accordeon.accessibility.description'),
|
|
||||||
content: <AccessibilitySettings />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'page_metadata',
|
|
||||||
image: <IconChartCandle />,
|
|
||||||
label: t('accordeon.pageMetadata.name'),
|
|
||||||
description: t('accordeon.pageMetadata.description'),
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
<DashboardTitleChanger />
|
|
||||||
<BrowserTabTitle />
|
|
||||||
<LogoImageChanger />
|
|
||||||
<FaviconChanger />
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'appereance',
|
|
||||||
image: <IconBrush />,
|
|
||||||
label: t('accordeon.appereance.name'),
|
|
||||||
description: t('accordeon.appereance.description'),
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
<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 />
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
if (env.NEXT_PUBLIC_NODE_ENV === 'development') {
|
|
||||||
items.push({
|
|
||||||
id: 'dev',
|
|
||||||
image: <IconCode />,
|
|
||||||
label: 'Developer options',
|
|
||||||
description: 'Options to help when developing',
|
|
||||||
content: (
|
|
||||||
<Stack>
|
|
||||||
<Checkbox
|
|
||||||
label="Use debug language"
|
|
||||||
defaultChecked={i18n?.language === 'cimode'}
|
|
||||||
description="This will show the translation keys instead of the actual translations"
|
|
||||||
onChange={(e) =>
|
|
||||||
// Change to CI mode language
|
|
||||||
i18n?.changeLanguage(e.target.checked ? 'cimode' : 'en')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { ScrollArea, Stack, Text } from '@mantine/core';
|
|
||||||
import { useViewportSize } from '@mantine/hooks';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
|
|
||||||
import { CustomizationSettingsAccordeon } from './CustomizationAccordeon';
|
|
||||||
|
|
||||||
export default function CustomizationSettings() {
|
|
||||||
const { height } = useViewportSize();
|
|
||||||
const { t } = useTranslation('settings/customization/general');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollArea style={{ height: height - 100 }} scrollbarSize={5}>
|
|
||||||
<Stack mt="xs" mb="md" spacing="xs">
|
|
||||||
<Text color="dimmed">{t('text')}</Text>
|
|
||||||
<CustomizationSettingsAccordeon />
|
|
||||||
</Stack>
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { Alert, Button, Grid, Input, LoadingOverlay, Slider } from '@mantine/core';
|
|
||||||
import { useForm } from '@mantine/form';
|
|
||||||
import { IconCheck, IconReload } from '@tabler/icons-react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
|
||||||
import { useConfigStore } from '../../../../config/store';
|
|
||||||
import { GridstackBreakpoints } from '../../../../constants/gridstack-breakpoints';
|
|
||||||
import { sleep } from '../../../../tools/client/time';
|
|
||||||
import { GridstackSettingsType } from '../../../../types/settings';
|
|
||||||
|
|
||||||
export const GridstackConfiguration = () => {
|
|
||||||
const { t } = useTranslation(['settings/customization/gridstack', 'common']);
|
|
||||||
const { config, name: configName } = useConfigContext();
|
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
|
||||||
|
|
||||||
if (!config || !configName) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialValue = config.settings.customization?.gridstack ?? {
|
|
||||||
columnCountSmall: 3,
|
|
||||||
columnCountMedium: 6,
|
|
||||||
columnCountLarge: 12,
|
|
||||||
};
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
initialValues: initialValue,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
|
|
||||||
const handleSubmit = async (values: GridstackSettingsType) => {
|
|
||||||
setIsSaving(true);
|
|
||||||
|
|
||||||
await sleep(250);
|
|
||||||
await updateConfig(
|
|
||||||
configName,
|
|
||||||
(previousConfig) => ({
|
|
||||||
...previousConfig,
|
|
||||||
settings: {
|
|
||||||
...previousConfig.settings,
|
|
||||||
customization: {
|
|
||||||
...previousConfig.settings.customization,
|
|
||||||
gridstack: values,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
form.resetDirty();
|
|
||||||
setIsSaving(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)} style={{ position: 'relative' }}>
|
|
||||||
<LoadingOverlay overlayBlur={2} visible={isSaving} radius="md" />
|
|
||||||
<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('columnCountSmall')} />
|
|
||||||
</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('columnCountMedium')} />
|
|
||||||
</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('columnCountLarge')} />
|
|
||||||
</Input.Wrapper>
|
|
||||||
{form.isDirty() && (
|
|
||||||
<Alert variant="light" color="yellow" title="Unsaved changes" my="md">
|
|
||||||
{t('unsavedChanges')}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
<Grid mt="md">
|
|
||||||
<Grid.Col md={6} xs={12}>
|
|
||||||
<Button variant="light" leftIcon={<IconCheck size={18} />} type="submit" fullWidth>
|
|
||||||
{t('applyChanges')}
|
|
||||||
</Button>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col md={6} xs={12}>
|
|
||||||
<Button
|
|
||||||
variant="light"
|
|
||||||
leftIcon={<IconReload size={18} />}
|
|
||||||
onClick={() =>
|
|
||||||
form.setValues({
|
|
||||||
columnCountSmall: 3,
|
|
||||||
columnCountMedium: 6,
|
|
||||||
columnCountLarge: 12,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
{t('defaultValues')}
|
|
||||||
</Button>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
import {
|
|
||||||
Checkbox,
|
|
||||||
Divider,
|
|
||||||
Flex,
|
|
||||||
Group,
|
|
||||||
Indicator,
|
|
||||||
Paper,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
createStyles,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
|
|
||||||
|
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
|
||||||
import { useConfigStore } from '../../../../config/store';
|
|
||||||
import { createDummyArray } from '../../../../tools/client/arrays';
|
|
||||||
import { CustomizationSettingsType } from '../../../../types/settings';
|
|
||||||
import { Logo } from '../../../layout/Logo';
|
|
||||||
|
|
||||||
export const LayoutSelector = () => {
|
|
||||||
const { classes } = useStyles();
|
|
||||||
|
|
||||||
const { config, name: configName } = useConfigContext();
|
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
|
||||||
|
|
||||||
const layoutSettings = config?.settings.customization.layout;
|
|
||||||
|
|
||||||
const [leftSidebar, setLeftSidebar] = useState(layoutSettings?.enabledLeftSidebar ?? true);
|
|
||||||
const [rightSidebar, setRightSidebar] = useState(layoutSettings?.enabledRightSidebar ?? true);
|
|
||||||
const [docker, setDocker] = useState(layoutSettings?.enabledDocker ?? false);
|
|
||||||
const [ping, setPing] = useState(layoutSettings?.enabledPing ?? false);
|
|
||||||
const [searchBar, setSearchBar] = useState(layoutSettings?.enabledSearchbar ?? false);
|
|
||||||
const { t } = useTranslation('settings/common');
|
|
||||||
|
|
||||||
if (!configName || !config) 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[key] = value;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
settings: {
|
|
||||||
...prev.settings,
|
|
||||||
customization: {
|
|
||||||
...prev.settings.customization,
|
|
||||||
layout,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const enabledPing = layoutSettings?.enabledPing ?? false;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Stack spacing={0} mb="md">
|
|
||||||
<Title order={6}>{t('layout.preview.title')}</Title>
|
|
||||||
<Text color="dimmed" size="xs">
|
|
||||||
{t('layout.preview.subtitle')}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Stack spacing="xs">
|
|
||||||
<Paper px="xs" py={4} withBorder>
|
|
||||||
<Group position="apart">
|
|
||||||
<Logo size="xs" />
|
|
||||||
<Group spacing={5}>
|
|
||||||
{searchBar && <PlaceholderElement width={60} height={10} />}
|
|
||||||
{docker && <PlaceholderElement width={10} height={10} />}
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Flex gap={6}>
|
|
||||||
{leftSidebar && (
|
|
||||||
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
|
||||||
<Flex gap={5} wrap="wrap">
|
|
||||||
{createDummyArray(5).map((item, index) => (
|
|
||||||
<PlaceholderElement
|
|
||||||
height={index % 4 === 0 ? 60 + 5 : 30}
|
|
||||||
width={30}
|
|
||||||
key={`example-item-right-sidebard-${index}`}
|
|
||||||
index={index}
|
|
||||||
hasPing={enabledPing}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</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 : 30}
|
|
||||||
key={`example-item-main-${index}`}
|
|
||||||
index={index}
|
|
||||||
hasPing={enabledPing}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{rightSidebar && (
|
|
||||||
<Paper className={classes.secondaryWrapper} p="xs" withBorder>
|
|
||||||
<Flex gap={5} align="start" wrap="wrap">
|
|
||||||
{createDummyArray(5).map((item, index) => (
|
|
||||||
<PlaceholderElement
|
|
||||||
height={30}
|
|
||||||
width={index % 4 === 0 ? 60 + 5 : 30}
|
|
||||||
key={`example-item-right-sidebard-${index}`}
|
|
||||||
index={index}
|
|
||||||
hasPing={enabledPing}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Divider label={t('layout.divider')} labelPosition="center" mt="md" mb="xs" />
|
|
||||||
<Stack spacing="xs">
|
|
||||||
<Checkbox
|
|
||||||
label={t('layout.enablelsidebar')}
|
|
||||||
description={t('layout.enablelsidebardesc')}
|
|
||||||
checked={leftSidebar}
|
|
||||||
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t('layout.enablersidebar')}
|
|
||||||
description={t('layout.enablersidebardesc')}
|
|
||||||
checked={rightSidebar}
|
|
||||||
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t('layout.enablesearchbar')}
|
|
||||||
checked={searchBar}
|
|
||||||
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t('layout.enabledocker')}
|
|
||||||
checked={docker}
|
|
||||||
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t('layout.enableping')}
|
|
||||||
checked={enabledPing}
|
|
||||||
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const PlaceholderElement = (props: any) => {
|
|
||||||
const { height, width, hasPing, index } = props;
|
|
||||||
|
|
||||||
if (hasPing) {
|
|
||||||
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} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useStyles = createStyles((theme) => ({
|
|
||||||
primaryWrapper: {
|
|
||||||
flexGrow: 2,
|
|
||||||
},
|
|
||||||
secondaryWrapper: {
|
|
||||||
flexGrow: 1,
|
|
||||||
maxWidth: 100,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
export const BackgroundChanger = () => {
|
|
||||||
const { t } = useTranslation('settings/customization/page-appearance');
|
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
|
||||||
const { config, name: configName } = useConfigContext();
|
|
||||||
const [backgroundImageUrl, setBackgroundImageUrl] = useState(
|
|
||||||
config?.settings.customization.backgroundImageUrl
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!configName) return null;
|
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
|
||||||
const { value } = ev.currentTarget;
|
|
||||||
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/backgrounds/background.png"
|
|
||||||
value={backgroundImageUrl}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
export const FaviconChanger = () => {
|
|
||||||
const { t } = useTranslation('settings/customization/page-appearance');
|
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
|
||||||
const { config, name: configName } = useConfigContext();
|
|
||||||
const [faviconUrl, setFaviconUrl] = useState(
|
|
||||||
config?.settings.customization.faviconUrl ?? '/imgs/favicon/favicon.svg'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!configName) return null;
|
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
|
||||||
const { value } = ev.currentTarget;
|
|
||||||
const faviconUrl = value.trim();
|
|
||||||
setFaviconUrl(faviconUrl);
|
|
||||||
updateConfig(configName, (prev) => ({
|
|
||||||
...prev,
|
|
||||||
settings: {
|
|
||||||
...prev.settings,
|
|
||||||
customization: {
|
|
||||||
...prev.settings.customization,
|
|
||||||
faviconUrl,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextInput
|
|
||||||
label={t('favicon.label')}
|
|
||||||
description={t('favicon.description')}
|
|
||||||
placeholder="/imgs/favicon/favicon.svg"
|
|
||||||
value={faviconUrl}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
export const LogoImageChanger = () => {
|
|
||||||
const { t } = useTranslation('settings/customization/page-appearance');
|
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
|
||||||
const { config, name: configName } = useConfigContext();
|
|
||||||
const [logoImageSrc, setLogoImageSrc] = useState(
|
|
||||||
config?.settings.customization.logoImageUrl ?? '/imgs/logo/logo.png'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!configName) return null;
|
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
|
||||||
const { value } = ev.currentTarget;
|
|
||||||
const logoImageSrc = value.trim();
|
|
||||||
setLogoImageSrc(logoImageSrc);
|
|
||||||
updateConfig(configName, (prev) => ({
|
|
||||||
...prev,
|
|
||||||
settings: {
|
|
||||||
...prev.settings,
|
|
||||||
customization: {
|
|
||||||
...prev.settings.customization,
|
|
||||||
logoImageUrl: logoImageSrc,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextInput
|
|
||||||
label={t('logo.label')}
|
|
||||||
description={t('logo.description')}
|
|
||||||
placeholder="/imgs/logo/logo.png"
|
|
||||||
value={logoImageSrc}
|
|
||||||
onChange={handleChange}
|
|
||||||
mb="sm"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
export const BrowserTabTitle = () => {
|
|
||||||
const { t } = useTranslation('settings/customization/page-appearance');
|
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
|
||||||
const { config, name: configName } = useConfigContext();
|
|
||||||
const [metaTitle, setMetaTitle] = useState(config?.settings.customization.metaTitle ?? '');
|
|
||||||
|
|
||||||
if (!configName) return null;
|
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
|
||||||
const { value } = ev.currentTarget;
|
|
||||||
const metaTitle = value.trim();
|
|
||||||
setMetaTitle(metaTitle);
|
|
||||||
updateConfig(configName, (prev) => ({
|
|
||||||
...prev,
|
|
||||||
settings: {
|
|
||||||
...prev.settings,
|
|
||||||
customization: {
|
|
||||||
...prev.settings.customization,
|
|
||||||
metaTitle,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextInput
|
|
||||||
label={t('metaTitle.label')}
|
|
||||||
description={t('metaTitle.description')}
|
|
||||||
placeholder="homarr - the best dashboard"
|
|
||||||
value={metaTitle}
|
|
||||||
onChange={handleChange}
|
|
||||||
mb="sm"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
export const DashboardTitleChanger = () => {
|
|
||||||
const { t } = useTranslation('settings/customization/page-appearance');
|
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
|
||||||
const { config, name: configName } = useConfigContext();
|
|
||||||
const [pageTitle, setPageTitle] = useState(config?.settings.customization.pageTitle ?? '');
|
|
||||||
|
|
||||||
if (!configName) return null;
|
|
||||||
|
|
||||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
|
||||||
const { value } = ev.currentTarget;
|
|
||||||
const pageTitle = value.trim();
|
|
||||||
setPageTitle(pageTitle);
|
|
||||||
updateConfig(configName, (prev) => ({
|
|
||||||
...prev,
|
|
||||||
settings: {
|
|
||||||
...prev.settings,
|
|
||||||
customization: {
|
|
||||||
...prev.settings.customization,
|
|
||||||
pageTitle,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextInput
|
|
||||||
label={t('pageTitle.label')}
|
|
||||||
description={t('pageTitle.description')}
|
|
||||||
placeholder="homarr"
|
|
||||||
value={pageTitle}
|
|
||||||
onChange={handleChange}
|
|
||||||
mb="sm"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import {
|
|
||||||
ColorSwatch,
|
|
||||||
Grid,
|
|
||||||
Group,
|
|
||||||
MantineTheme,
|
|
||||||
Popover,
|
|
||||||
Text,
|
|
||||||
useMantineTheme,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
|
||||||
import { useConfigStore } from '../../../../config/store';
|
|
||||||
import { useColorTheme } from '../../../../tools/color';
|
|
||||||
|
|
||||||
interface ColorControlProps {
|
|
||||||
defaultValue: MantineTheme['primaryColor'] | undefined;
|
|
||||||
type: 'primary' | 'secondary';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ColorSelector({ type, defaultValue }: ColorControlProps) {
|
|
||||||
const { t } = useTranslation('settings/customization/color-selector');
|
|
||||||
const { config, name: configName } = useConfigContext();
|
|
||||||
const [color, setColor] =
|
|
||||||
type === 'primary'
|
|
||||||
? useState(config?.settings.customization.colors.primary || defaultValue)
|
|
||||||
: useState(config?.settings.customization.colors.secondary || defaultValue);
|
|
||||||
const [popoverOpened, popover] = useDisclosure(false);
|
|
||||||
const { setPrimaryColor, setSecondaryColor } = useColorTheme();
|
|
||||||
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[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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import { Box, Group, Loader, Stack, Text, createStyles, useMantineTheme } from '@mantine/core';
|
|
||||||
import { useDebouncedValue } from '@mantine/hooks';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { highlight, languages } from 'prismjs';
|
|
||||||
import 'prismjs/components/prism-css';
|
|
||||||
import 'prismjs/themes/prism.css';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import Editor from 'react-simple-code-editor';
|
|
||||||
|
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
|
||||||
import { useConfigStore } from '../../../../config/store';
|
|
||||||
|
|
||||||
export const CustomCssChanger = () => {
|
|
||||||
const { t } = useTranslation('settings/customization/page-appearance');
|
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
|
||||||
const { colorScheme, colors } = useMantineTheme();
|
|
||||||
const { config, name: configName } = useConfigContext();
|
|
||||||
const [nonDebouncedCustomCSS, setNonDebouncedCustomCSS] = useState(
|
|
||||||
config?.settings.customization.customCss ?? ''
|
|
||||||
);
|
|
||||||
const [debouncedCustomCSS] = useDebouncedValue(nonDebouncedCustomCSS, 696);
|
|
||||||
const { classes } = useStyles();
|
|
||||||
|
|
||||||
if (!configName) return null;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateConfig(configName, (prev) => ({
|
|
||||||
...prev,
|
|
||||||
settings: {
|
|
||||||
...prev.settings,
|
|
||||||
customization: {
|
|
||||||
...prev.settings.customization,
|
|
||||||
customCss: debouncedCustomCSS,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}, [debouncedCustomCSS]);
|
|
||||||
|
|
||||||
const codeIsDirty = nonDebouncedCustomCSS !== debouncedCustomCSS;
|
|
||||||
const codeEditorHeight = codeIsDirty ? 250 - 42 : 250;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack spacing={4} mt="xl">
|
|
||||||
<Text>{t('customCSS.label')}</Text>
|
|
||||||
<Text color="dimmed" size="xs">
|
|
||||||
{t('customCSS.description')}
|
|
||||||
</Text>
|
|
||||||
<div className={classes.codeEditorRoot}>
|
|
||||||
<Editor
|
|
||||||
value={nonDebouncedCustomCSS}
|
|
||||||
onValueChange={(code) => setNonDebouncedCustomCSS(code)}
|
|
||||||
highlight={(code) => highlight(code, languages.extend('css', {}), 'css')}
|
|
||||||
padding={10}
|
|
||||||
style={{
|
|
||||||
fontFamily: '"Fira code", "Fira Mono", monospace',
|
|
||||||
fontSize: 12,
|
|
||||||
minHeight: codeEditorHeight,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{codeIsDirty && (
|
|
||||||
<Box className={classes.codeEditorFooter}>
|
|
||||||
<Group p="xs" spacing="xs">
|
|
||||||
<Loader color={colors.gray[0]} size={18} />
|
|
||||||
<Text>{t('customCSS.applying')}</Text>
|
|
||||||
</Group>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useStyles = createStyles(({ colors, colorScheme, radius }) => ({
|
|
||||||
codeEditorFooter: {
|
|
||||||
borderBottomLeftRadius: radius.sm,
|
|
||||||
borderBottomRightRadius: radius.sm,
|
|
||||||
backgroundColor: colorScheme === 'dark' ? colors.dark[7] : undefined,
|
|
||||||
},
|
|
||||||
codeEditorRoot: {
|
|
||||||
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],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { Slider, Stack, Text } from '@mantine/core';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
|
||||||
import { useConfigStore } from '../../../../config/store';
|
|
||||||
|
|
||||||
export function DashboardTilesOpacitySelector() {
|
|
||||||
const { config, name: configName } = useConfigContext();
|
|
||||||
const [opacity, setOpacity] = useState(config?.settings.customization.appOpacity || 100);
|
|
||||||
const { t } = useTranslation('settings/customization/opacity-selector');
|
|
||||||
|
|
||||||
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" mb="md">
|
|
||||||
<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' },
|
|
||||||
];
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import {
|
|
||||||
ColorSwatch,
|
|
||||||
Grid,
|
|
||||||
Group,
|
|
||||||
MantineTheme,
|
|
||||||
Popover,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
useMantineTheme,
|
|
||||||
} from '@mantine/core';
|
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
|
||||||
import { useConfigStore } from '../../../../config/store';
|
|
||||||
import { useColorTheme } from '../../../../tools/color';
|
|
||||||
|
|
||||||
export function ShadeSelector() {
|
|
||||||
const { t } = useTranslation('settings/customization/shade-selector');
|
|
||||||
const { config, name: configName } = useConfigContext();
|
|
||||||
const [shade, setShade] = useState(config?.settings.customization.colors.shade);
|
|
||||||
const [popoverOpened, popover] = useDisclosure(false);
|
|
||||||
const { primaryColor, setPrimaryShade } = useColorTheme();
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { Drawer, Tabs, Title } from '@mantine/core';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
|
|
||||||
import { useConfigContext } from '../../config/provider';
|
|
||||||
import { useConfigStore } from '../../config/store';
|
|
||||||
import CommonSettings from './Common/CommonSettings';
|
|
||||||
import CustomizationSettings from './Customization/CustomizationSettings';
|
|
||||||
|
|
||||||
function SettingsMenu({ newVersionAvailable }: { newVersionAvailable: string }) {
|
|
||||||
const { t } = useTranslation('settings/common');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tabs defaultValue="common">
|
|
||||||
<Tabs.List grow>
|
|
||||||
<Tabs.Tab value="common">{t('tabs.common')}</Tabs.Tab>
|
|
||||||
<Tabs.Tab value="customization">{t('tabs.customizations')}</Tabs.Tab>
|
|
||||||
</Tabs.List>
|
|
||||||
<Tabs.Panel data-autofocus value="common">
|
|
||||||
<CommonSettings />
|
|
||||||
</Tabs.Panel>
|
|
||||||
<Tabs.Panel value="customization">
|
|
||||||
<CustomizationSettings />
|
|
||||||
</Tabs.Panel>
|
|
||||||
</Tabs>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SettingsDrawerProps {
|
|
||||||
opened: boolean;
|
|
||||||
closeDrawer: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SettingsDrawer({
|
|
||||||
opened,
|
|
||||||
closeDrawer,
|
|
||||||
newVersionAvailable,
|
|
||||||
}: SettingsDrawerProps & { newVersionAvailable: string }) {
|
|
||||||
const { t } = useTranslation('settings/common');
|
|
||||||
const { config, name: configName } = useConfigContext();
|
|
||||||
const { updateConfig } = useConfigStore();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
size="lg"
|
|
||||||
padding="lg"
|
|
||||||
position="right"
|
|
||||||
title={<Title order={5}>{t('title')}</Title>}
|
|
||||||
opened={opened}
|
|
||||||
onClose={() => {
|
|
||||||
closeDrawer();
|
|
||||||
if (!configName || !config) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConfig(configName, (_) => config, false, true);
|
|
||||||
}}
|
|
||||||
transitionProps={{ transition: 'slide-left' }}
|
|
||||||
>
|
|
||||||
<SettingsMenu newVersionAvailable={newVersionAvailable} />
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Global } from '@mantine/core';
|
|
||||||
|
|
||||||
import { useConfigContext } from '../../config/provider';
|
|
||||||
|
|
||||||
export function Background() {
|
|
||||||
const { config } = useConfigContext();
|
|
||||||
|
|
||||||
if (!config?.settings.customization.backgroundImageUrl) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Global
|
|
||||||
styles={{
|
|
||||||
body: {
|
|
||||||
minHeight: '100vh',
|
|
||||||
backgroundImage: `url('${config?.settings.customization.backgroundImageUrl}')`,
|
|
||||||
backgroundPosition: 'center center',
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Group, Image, Text } from '@mantine/core';
|
import { Group, Image, Text } from '@mantine/core';
|
||||||
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
||||||
|
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../../config/provider';
|
||||||
import { usePrimaryGradient } from './useGradient';
|
import { usePrimaryGradient } from './useGradient';
|
||||||
|
|
||||||
interface LogoProps {
|
interface LogoProps {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createStyles } from '@mantine/core';
|
import { createStyles } from '@mantine/core';
|
||||||
|
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../../config/provider';
|
||||||
|
|
||||||
export const useCardStyles = (isCategory: boolean) => {
|
export const useCardStyles = (isCategory: boolean) => {
|
||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { MantineGradient } from '@mantine/core';
|
import { MantineGradient } from '@mantine/core';
|
||||||
|
|
||||||
import { useColorTheme } from '../../tools/color';
|
import { useColorTheme } from '../../../tools/color';
|
||||||
|
|
||||||
export const usePrimaryGradient = (): MantineGradient => {
|
export const usePrimaryGradient = (): MantineGradient => {
|
||||||
const { primaryColor, secondaryColor } = useColorTheme();
|
const { primaryColor, secondaryColor } = useColorTheme();
|
||||||
31
src/components/layout/Meta/BoardHead.tsx
Normal file
31
src/components/layout/Meta/BoardHead.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import Head from 'next/head';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useConfigContext } from '../../../config/provider';
|
||||||
|
|
||||||
|
export const BoardHeadOverride = () => {
|
||||||
|
const { config } = useConfigContext();
|
||||||
|
|
||||||
|
if (!config) return null;
|
||||||
|
|
||||||
|
const { metaTitle, faviconUrl } = config.settings.customization;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Head>
|
||||||
|
{metaTitle && metaTitle.length > 0 && (
|
||||||
|
<>
|
||||||
|
<title>{metaTitle}</title>
|
||||||
|
<meta name="apple-mobile-web-app-title" content={metaTitle} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{faviconUrl && faviconUrl.length > 0 && (
|
||||||
|
<>
|
||||||
|
<link rel="shortcut icon" href={faviconUrl} />
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" href={faviconUrl} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Head>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
|
import { useMantineTheme } from '@mantine/core';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
interface CommonHeaderProps {
|
export const CommonHead = () => {
|
||||||
children?: ReactNode;
|
const { colorScheme } = useMantineTheme();
|
||||||
}
|
|
||||||
|
|
||||||
export const CommonHeader = ({ children }: CommonHeaderProps) => {
|
|
||||||
return (
|
return (
|
||||||
<Head>
|
<Head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||||
<link rel="shortcut icon" href="/imgs/favicon/favicon.svg" />
|
<link rel="shortcut icon" href="/imgs/favicon/favicon.svg" />
|
||||||
|
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
@@ -18,7 +17,10 @@ export const CommonHeader = ({ children }: CommonHeaderProps) => {
|
|||||||
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
|
||||||
{children}
|
<meta
|
||||||
|
name="apple-mobile-web-app-status-bar-style"
|
||||||
|
content={colorScheme === 'dark' ? 'white-translucent' : 'black-translucent'}
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
/* eslint-disable react/no-invalid-html-attribute */
|
|
||||||
import NextHead from 'next/head';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { useConfigContext } from '../../../config/provider';
|
|
||||||
import { SafariStatusBarStyle } from './SafariStatusBarStyle';
|
|
||||||
|
|
||||||
export function Head() {
|
|
||||||
const { config } = useConfigContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NextHead>
|
|
||||||
<title>{config?.settings.customization.metaTitle || 'Homarr 🦞'}</title>
|
|
||||||
<link
|
|
||||||
rel="shortcut icon"
|
|
||||||
href={config?.settings.customization.faviconUrl || '/imgs/favicon/favicon.svg'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
|
||||||
|
|
||||||
{/* configure apple splash screen & touch icon */}
|
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
href={config?.settings.customization.faviconUrl || '/imgs/favicon/favicon.svg'}
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="apple-mobile-web-app-title"
|
|
||||||
content={config?.settings.customization.metaTitle || 'Homarr'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SafariStatusBarStyle />
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
</NextHead>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { useMantineTheme } from '@mantine/core';
|
|
||||||
|
|
||||||
export const SafariStatusBarStyle = () => {
|
|
||||||
const { colorScheme } = useMantineTheme();
|
|
||||||
const isDark = colorScheme === 'dark';
|
|
||||||
return (
|
|
||||||
<meta
|
|
||||||
name="apple-mobile-web-app-status-bar-style"
|
|
||||||
content={isDark ? 'white-translucent' : 'black-translucent'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button, Text, Title, Tooltip, clsx } from '@mantine/core';
|
import { Button, Global, Text, Title, Tooltip, clsx } from '@mantine/core';
|
||||||
import { useHotkeys, useWindowEvent } from '@mantine/hooks';
|
import { useHotkeys, useWindowEvent } from '@mantine/hooks';
|
||||||
import { openContextModal } from '@mantine/modals';
|
import { openContextModal } from '@mantine/modals';
|
||||||
import { hideNotification, showNotification } from '@mantine/notifications';
|
import { hideNotification, showNotification } from '@mantine/notifications';
|
||||||
@@ -19,8 +19,8 @@ import { useConfigContext } from '~/config/provider';
|
|||||||
import { env } from '~/env';
|
import { env } from '~/env';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
import { Background } from '../Background';
|
|
||||||
import { HeaderActionButton } from '../Header/ActionButton';
|
import { HeaderActionButton } from '../Header/ActionButton';
|
||||||
|
import { BoardHeadOverride } from '../Meta/BoardHead';
|
||||||
import { MainLayout } from './MainLayout';
|
import { MainLayout } from './MainLayout';
|
||||||
|
|
||||||
type BoardLayoutProps = {
|
type BoardLayoutProps = {
|
||||||
@@ -32,7 +32,8 @@ export const BoardLayout = ({ children }: BoardLayoutProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout headerActions={<HeaderActions />}>
|
<MainLayout headerActions={<HeaderActions />}>
|
||||||
<Background />
|
<BoardHeadOverride />
|
||||||
|
<BackgroundImage />
|
||||||
{children}
|
{children}
|
||||||
<style>{clsx(config?.settings.customization.customCss)}</style>
|
<style>{clsx(config?.settings.customization.customCss)}</style>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
@@ -195,3 +196,25 @@ const AddElementButton = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BackgroundImage = () => {
|
||||||
|
const { config } = useConfigContext();
|
||||||
|
|
||||||
|
if (!config?.settings.customization.backgroundImageUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Global
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
minHeight: '100vh',
|
||||||
|
backgroundImage: `url('${config?.settings.customization.backgroundImageUrl}')`,
|
||||||
|
backgroundPosition: 'center center',
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { AppShell, useMantineTheme } from '@mantine/core';
|
import { AppShell, useMantineTheme } from '@mantine/core';
|
||||||
|
|
||||||
import { MainHeader } from '../Header/Header';
|
import { MainHeader } from '../Header/Header';
|
||||||
import { Head } from '../Meta/Head';
|
|
||||||
|
|
||||||
type MainLayoutProps = {
|
type MainLayoutProps = {
|
||||||
headerActions?: React.ReactNode;
|
headerActions?: React.ReactNode;
|
||||||
@@ -21,7 +20,6 @@ export const MainLayout = ({ headerActions, children }: MainLayoutProps) => {
|
|||||||
header={<MainHeader headerActions={headerActions} />}
|
header={<MainHeader headerActions={headerActions} />}
|
||||||
className="dashboard-app-shell"
|
className="dashboard-app-shell"
|
||||||
>
|
>
|
||||||
<Head />
|
|
||||||
{children}
|
{children}
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
|||||||
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
||||||
|
|
||||||
import { MainHeader } from '../Header/Header';
|
import { MainHeader } from '../Header/Header';
|
||||||
import { CommonHeader } from '../common-header';
|
|
||||||
|
|
||||||
interface ManageLayoutProps {
|
interface ManageLayoutProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -143,7 +142,6 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CommonHeader />
|
|
||||||
<AppShell
|
<AppShell
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ import { useConfigContext } from '../../../../config/provider';
|
|||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
|
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
|
||||||
import { useColorTheme } from '../../../../tools/color';
|
import { useColorTheme } from '../../../../tools/color';
|
||||||
import Credits from '../../../Settings/Common/Credits';
|
import { usePrimaryGradient } from '../../Common/useGradient';
|
||||||
import Tip from '../../../layout/Tip';
|
import Credits from './Credits';
|
||||||
import { usePrimaryGradient } from '../../../layout/useGradient';
|
import Tip from './Tip';
|
||||||
|
|
||||||
interface AboutModalProps {
|
interface AboutModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
@@ -2,7 +2,7 @@ import { Anchor, Box, Collapse, Flex, Table, Text } from '@mantine/core';
|
|||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
import { usePackageAttributesStore } from '../../../tools/client/zustands/usePackageAttributesStore';
|
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
|
||||||
|
|
||||||
export default function Credits() {
|
export default function Credits() {
|
||||||
const { t } = useTranslation('settings/common');
|
const { t } = useTranslation('settings/common');
|
||||||
@@ -2,7 +2,7 @@ import { Button, ButtonProps } from '@mantine/core';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ForwardedRef, forwardRef } from 'react';
|
import { ForwardedRef, forwardRef } from 'react';
|
||||||
|
|
||||||
import { useCardStyles } from '../useCardStyles';
|
import { useCardStyles } from '../Common/useCardStyles';
|
||||||
|
|
||||||
type SpecificLinkProps = {
|
type SpecificLinkProps = {
|
||||||
component: typeof Link;
|
component: typeof Link;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { User } from 'next-auth';
|
|||||||
import { signOut, useSession } from 'next-auth/react';
|
import { signOut, useSession } from 'next-auth/react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { AboutModal } from '~/components/Dashboard/Modals/AboutModal/AboutModal';
|
import { AboutModal } from '~/components/layout/Header/About/AboutModal';
|
||||||
import { useColorScheme } from '~/hooks/use-colorscheme';
|
import { useColorScheme } from '~/hooks/use-colorscheme';
|
||||||
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { IconAlertTriangle } from '@tabler/icons-react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
||||||
|
|
||||||
import { Logo } from '../Logo';
|
import { Logo } from '../Common/Logo';
|
||||||
import { AvatarMenu } from './AvatarMenu';
|
import { AvatarMenu } from './AvatarMenu';
|
||||||
import { Search } from './Search';
|
import { Search } from './Search';
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { ReactNode, forwardRef, useEffect, useMemo, useRef, useState } from 'rea
|
|||||||
import { useConfigContext } from '~/config/provider';
|
import { useConfigContext } from '~/config/provider';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
import { MovieModal } from './MovieModal';
|
import { MovieModal } from './Search/MovieModal';
|
||||||
|
|
||||||
type SearchProps = {
|
type SearchProps = {
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useTranslation } from 'next-i18next';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
import { useCardStyles } from '../../components/layout/useCardStyles';
|
import { useCardStyles } from '../../components/layout/Common/useCardStyles';
|
||||||
import { useConfigContext } from '../../config/provider';
|
import { useConfigContext } from '../../config/provider';
|
||||||
import ContainerActionBar from './ContainerActionBar';
|
import ContainerActionBar from './ContainerActionBar';
|
||||||
import DockerTable from './DockerTable';
|
import DockerTable from './DockerTable';
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { AppProps } from 'next/app';
|
|||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import 'video.js/dist/video-js.css';
|
import 'video.js/dist/video-js.css';
|
||||||
|
import { CommonHead } from '~/components/layout/Meta/CommonHead';
|
||||||
import { env } from '~/env.js';
|
import { env } from '~/env.js';
|
||||||
import { ColorSchemeProvider } from '~/hooks/use-colorscheme';
|
import { ColorSchemeProvider } from '~/hooks/use-colorscheme';
|
||||||
import { modals } from '~/modals/modals';
|
import { modals } from '~/modals/modals';
|
||||||
@@ -79,9 +80,7 @@ function App(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<CommonHead />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
|
||||||
</Head>
|
|
||||||
<SessionProvider session={pageProps.session}>
|
<SessionProvider session={pageProps.session}>
|
||||||
<PersistQueryClientProvider
|
<PersistQueryClientProvider
|
||||||
client={queryClient}
|
client={queryClient}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import { GetServerSideProps } from 'next';
|
|||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { CommonHeader } from '~/components/layout/common-header';
|
|
||||||
import { getServerAuthSession } from '~/server/auth';
|
import { getServerAuthSession } from '~/server/auth';
|
||||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||||
import { signInSchema } from '~/validations/user';
|
import { signInSchema } from '~/validations/user';
|
||||||
@@ -46,9 +46,9 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex h="100dvh" display="flex" w="100%" direction="column" align="center" justify="center">
|
<Flex h="100dvh" display="flex" w="100%" direction="column" align="center" justify="center">
|
||||||
<CommonHeader>
|
<Head>
|
||||||
<title>Login • Homarr</title>
|
<title>Login • Homarr</title>
|
||||||
</CommonHeader>
|
</Head>
|
||||||
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={420}>
|
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={420}>
|
||||||
<Title align="center" weight={900}>
|
<Title align="center" weight={900}>
|
||||||
{t('title')}
|
{t('title')}
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import {
|
|||||||
IconPlus,
|
IconPlus,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||||
import { CommonHeader } from '~/components/layout/common-header';
|
|
||||||
import { sleep } from '~/tools/client/time';
|
import { sleep } from '~/tools/client/time';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@ const BoardsPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ManageLayout>
|
<ManageLayout>
|
||||||
<CommonHeader>
|
<Head>
|
||||||
<title>Boards • Homarr</title>
|
<title>Boards • Homarr</title>
|
||||||
</CommonHeader>
|
</Head>
|
||||||
|
|
||||||
<Title mb="xl">Boards</Title>
|
<Title mb="xl">Boards</Title>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Text, Title } from '@mantine/core';
|
import { Text, Title } from '@mantine/core';
|
||||||
|
import Head from 'next/head';
|
||||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||||
import { CommonHeader } from '~/components/layout/common-header';
|
|
||||||
|
|
||||||
const SettingsPage = () => {
|
const SettingsPage = () => {
|
||||||
return (
|
return (
|
||||||
<ManageLayout>
|
<ManageLayout>
|
||||||
<CommonHeader>
|
<Head>
|
||||||
<title>Settings • Homarr</title>
|
<title>Settings • Homarr</title>
|
||||||
</CommonHeader>
|
</Head>
|
||||||
|
|
||||||
<Title>Settings</Title>
|
<Title>Settings</Title>
|
||||||
<Text>Coming soon!</Text>
|
<Text>Coming soon!</Text>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { Button, Group, Select, Stack, Text, Title } from '@mantine/core';
|
|||||||
import { createFormContext } from '@mantine/form';
|
import { createFormContext } from '@mantine/form';
|
||||||
import type { InferGetServerSidePropsType } from 'next';
|
import type { InferGetServerSidePropsType } from 'next';
|
||||||
import { GetServerSidePropsContext } from 'next';
|
import { GetServerSidePropsContext } from 'next';
|
||||||
|
import Head from 'next/head';
|
||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { AccessibilitySettings } from '~/components/Settings/Customization/Accessibility/AccessibilitySettings';
|
import { AccessibilitySettings } from '~/components/User/Preferences/AccessibilitySettings';
|
||||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||||
import { CommonHeader } from '~/components/layout/common-header';
|
|
||||||
import { languages } from '~/tools/language';
|
import { languages } from '~/tools/language';
|
||||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||||
import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
||||||
@@ -20,9 +20,9 @@ const PreferencesPage = ({ locale }: InferGetServerSidePropsType<typeof getServe
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ManageLayout>
|
<ManageLayout>
|
||||||
<CommonHeader>
|
<Head>
|
||||||
<title>Preferences • Homarr</title>
|
<title>Preferences • Homarr</title>
|
||||||
</CommonHeader>
|
</Head>
|
||||||
<Title mb="xl">Preferences</Title>
|
<Title mb="xl">Preferences</Title>
|
||||||
|
|
||||||
{data && <SettingsComponent settings={data.settings} />}
|
{data && <SettingsComponent settings={data.settings} />}
|
||||||
@@ -141,7 +141,12 @@ const SelectItem = forwardRef<HTMLDivElement, ItemProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export async function getServerSideProps({ req, res, locale }: GetServerSidePropsContext) {
|
export async function getServerSideProps({ req, res, locale }: GetServerSidePropsContext) {
|
||||||
const translations = await getServerSideTranslations(manageNamespaces, locale, undefined, undefined);
|
const translations = await getServerSideTranslations(
|
||||||
|
manageNamespaces,
|
||||||
|
locale,
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...translations,
|
...translations,
|
||||||
|
|||||||
Reference in New Issue
Block a user