♻️ Improved code structure for layout, remove most settings components

This commit is contained in:
Meier Lukas
2023-08-01 15:23:31 +02:00
parent 6b8d94b6b5
commit 65d0b31a1a
48 changed files with 103 additions and 1575 deletions

View File

@@ -1,5 +1,5 @@
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';
type LayoutPreviewProps = {

View File

@@ -4,7 +4,7 @@ import { motion } from 'framer-motion';
import Link from 'next/link';
import { AppType } from '../../../../types/app';
import { useCardStyles } from '../../../layout/useCardStyles';
import { useCardStyles } from '../../../layout/Common/useCardStyles';
import { useEditModeStore } from '../../Views/useEditModeStore';
import { HomarrCardWrapper } from '../HomarrCardWrapper';
import { BaseTileProps } from '../type';

View File

@@ -1,7 +1,7 @@
import { Card, CardProps } from '@mantine/core';
import { ReactNode } from 'react';
import { useCardStyles } from '../../layout/useCardStyles';
import { useCardStyles } from '../../layout/Common/useCardStyles';
import { useEditModeStore } from '../Views/useEditModeStore';
interface HomarrCardWrapperProps extends CardProps {

View File

@@ -16,7 +16,7 @@ import { useTranslation } from 'next-i18next';
import { useConfigContext } from '../../../../config/provider';
import { CategoryType } from '../../../../types/category';
import { useCardStyles } from '../../../layout/useCardStyles';
import { useCardStyles } from '../../../layout/Common/useCardStyles';
import { useEditModeStore } from '../../Views/useEditModeStore';
import { WrapperContent } from '../WrapperContent';
import { useGridstack } from '../gridstack/use-gridstack';

View File

@@ -1,7 +1,7 @@
import { Card } from '@mantine/core';
import { RefObject } from 'react';
import { useCardStyles } from '../../../layout/useCardStyles';
import { useCardStyles } from '../../../layout/Common/useCardStyles';
import { WrapperContent } from '../WrapperContent';
import { useGridstack } from '../gridstack/use-gridstack';

View File

@@ -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>
);
}

View File

@@ -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,
},
}));

View File

@@ -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 }),
});
},
});
};

View File

@@ -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;
};

View File

@@ -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>
);
}

View File

@@ -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>
);
};

View File

@@ -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,
},
}));

View File

@@ -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}
/>
);
};

View File

@@ -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}
/>
);
};

View File

@@ -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"
/>
);
};

View File

@@ -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"
/>
);
};

View File

@@ -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"
/>
);
};

View File

@@ -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>
);
}

View File

@@ -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],
},
},
}));

View File

@@ -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' },
];

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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',
},
}}
/>
);
}

View File

@@ -1,7 +1,7 @@
import { Group, Image, Text } from '@mantine/core';
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { useConfigContext } from '../../config/provider';
import { useConfigContext } from '../../../config/provider';
import { usePrimaryGradient } from './useGradient';
interface LogoProps {

View File

@@ -1,6 +1,6 @@
import { createStyles } from '@mantine/core';
import { useConfigContext } from '../../config/provider';
import { useConfigContext } from '../../../config/provider';
export const useCardStyles = (isCategory: boolean) => {
const { config } = useConfigContext();

View File

@@ -1,6 +1,6 @@
import { MantineGradient } from '@mantine/core';
import { useColorTheme } from '../../tools/color';
import { useColorTheme } from '../../../tools/color';
export const usePrimaryGradient = (): MantineGradient => {
const { primaryColor, secondaryColor } = useColorTheme();

View 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>
);
};

View File

@@ -1,13 +1,12 @@
import { useMantineTheme } from '@mantine/core';
import Head from 'next/head';
import { ReactNode } from 'react';
interface CommonHeaderProps {
children?: ReactNode;
}
export const CommonHead = () => {
const { colorScheme } = useMantineTheme();
export const CommonHeader = ({ children }: CommonHeaderProps) => {
return (
<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="manifest" href="/site.webmanifest" />
@@ -18,7 +17,10 @@ export const CommonHeader = ({ children }: CommonHeaderProps) => {
<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>
);
};

View File

@@ -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>
);
}

View File

@@ -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'}
/>
);
};

View File

@@ -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 { openContextModal } from '@mantine/modals';
import { hideNotification, showNotification } from '@mantine/notifications';
@@ -19,8 +19,8 @@ import { useConfigContext } from '~/config/provider';
import { env } from '~/env';
import { api } from '~/utils/api';
import { Background } from '../Background';
import { HeaderActionButton } from '../Header/ActionButton';
import { BoardHeadOverride } from '../Meta/BoardHead';
import { MainLayout } from './MainLayout';
type BoardLayoutProps = {
@@ -32,7 +32,8 @@ export const BoardLayout = ({ children }: BoardLayoutProps) => {
return (
<MainLayout headerActions={<HeaderActions />}>
<Background />
<BoardHeadOverride />
<BackgroundImage />
{children}
<style>{clsx(config?.settings.customization.customCss)}</style>
</MainLayout>
@@ -195,3 +196,25 @@ const AddElementButton = () => {
</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',
},
}}
/>
);
};

View File

@@ -1,7 +1,6 @@
import { AppShell, useMantineTheme } from '@mantine/core';
import { MainHeader } from '../Header/Header';
import { Head } from '../Meta/Head';
type MainLayoutProps = {
headerActions?: React.ReactNode;
@@ -21,7 +20,6 @@ export const MainLayout = ({ headerActions, children }: MainLayoutProps) => {
header={<MainHeader headerActions={headerActions} />}
className="dashboard-app-shell"
>
<Head />
{children}
</AppShell>
);

View File

@@ -33,7 +33,6 @@ import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
import { MainHeader } from '../Header/Header';
import { CommonHeader } from '../common-header';
interface ManageLayoutProps {
children: ReactNode;
@@ -143,7 +142,6 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
return (
<>
<CommonHeader />
<AppShell
styles={{
root: {

View File

@@ -36,9 +36,9 @@ import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store';
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
import { useColorTheme } from '../../../../tools/color';
import Credits from '../../../Settings/Common/Credits';
import Tip from '../../../layout/Tip';
import { usePrimaryGradient } from '../../../layout/useGradient';
import { usePrimaryGradient } from '../../Common/useGradient';
import Credits from './Credits';
import Tip from './Tip';
interface AboutModalProps {
opened: boolean;

View File

@@ -2,7 +2,7 @@ import { Anchor, Box, Collapse, Flex, Table, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useTranslation } from 'next-i18next';
import { usePackageAttributesStore } from '../../../tools/client/zustands/usePackageAttributesStore';
import { usePackageAttributesStore } from '../../../../tools/client/zustands/usePackageAttributesStore';
export default function Credits() {
const { t } = useTranslation('settings/common');

View File

@@ -2,7 +2,7 @@ import { Button, ButtonProps } from '@mantine/core';
import Link from 'next/link';
import { ForwardedRef, forwardRef } from 'react';
import { useCardStyles } from '../useCardStyles';
import { useCardStyles } from '../Common/useCardStyles';
type SpecificLinkProps = {
component: typeof Link;

View File

@@ -14,7 +14,7 @@ import { User } from 'next-auth';
import { signOut, useSession } from 'next-auth/react';
import Link from 'next/link';
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 { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';

View File

@@ -13,7 +13,7 @@ import { IconAlertTriangle } from '@tabler/icons-react';
import Link from 'next/link';
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
import { Logo } from '../Logo';
import { Logo } from '../Common/Logo';
import { AvatarMenu } from './AvatarMenu';
import { Search } from './Search';

View File

@@ -13,7 +13,7 @@ import { ReactNode, forwardRef, useEffect, useMemo, useRef, useState } from 'rea
import { useConfigContext } from '~/config/provider';
import { api } from '~/utils/api';
import { MovieModal } from './MovieModal';
import { MovieModal } from './Search/MovieModal';
type SearchProps = {
isMobile?: boolean;

View File

@@ -6,7 +6,7 @@ import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { api } from '~/utils/api';
import { useCardStyles } from '../../components/layout/useCardStyles';
import { useCardStyles } from '../../components/layout/Common/useCardStyles';
import { useConfigContext } from '../../config/provider';
import ContainerActionBar from './ContainerActionBar';
import DockerTable from './DockerTable';

View File

@@ -16,6 +16,7 @@ import { AppProps } from 'next/app';
import Head from 'next/head';
import { useEffect, useState } from 'react';
import 'video.js/dist/video-js.css';
import { CommonHead } from '~/components/layout/Meta/CommonHead';
import { env } from '~/env.js';
import { ColorSchemeProvider } from '~/hooks/use-colorscheme';
import { modals } from '~/modals/modals';
@@ -79,9 +80,7 @@ function App(
return (
<>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
</Head>
<CommonHead />
<SessionProvider session={pageProps.session}>
<PersistQueryClientProvider
client={queryClient}

View File

@@ -15,9 +15,9 @@ import { GetServerSideProps } from 'next';
import { signIn } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { z } from 'zod';
import { CommonHeader } from '~/components/layout/common-header';
import { getServerAuthSession } from '~/server/auth';
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
import { signInSchema } from '~/validations/user';
@@ -46,9 +46,9 @@ export default function LoginPage() {
return (
<Flex h="100dvh" display="flex" w="100%" direction="column" align="center" justify="center">
<CommonHeader>
<Head>
<title>Login Homarr</title>
</CommonHeader>
</Head>
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={420}>
<Title align="center" weight={900}>
{t('title')}

View File

@@ -21,9 +21,9 @@ import {
IconPlus,
IconTrash,
} from '@tabler/icons-react';
import Head from 'next/head';
import Link from 'next/link';
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
import { CommonHeader } from '~/components/layout/common-header';
import { sleep } from '~/tools/client/time';
import { api } from '~/utils/api';
@@ -34,9 +34,9 @@ const BoardsPage = () => {
return (
<ManageLayout>
<CommonHeader>
<Head>
<title>Boards Homarr</title>
</CommonHeader>
</Head>
<Title mb="xl">Boards</Title>

View File

@@ -1,13 +1,13 @@
import { Text, Title } from '@mantine/core';
import Head from 'next/head';
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
import { CommonHeader } from '~/components/layout/common-header';
const SettingsPage = () => {
return (
<ManageLayout>
<CommonHeader>
<Head>
<title>Settings Homarr</title>
</CommonHeader>
</Head>
<Title>Settings</Title>
<Text>Coming soon!</Text>

View File

@@ -2,12 +2,12 @@ import { Button, Group, Select, Stack, Text, Title } from '@mantine/core';
import { createFormContext } from '@mantine/form';
import type { InferGetServerSidePropsType } from 'next';
import { GetServerSidePropsContext } from 'next';
import Head from 'next/head';
import { forwardRef } from 'react';
import { useTranslation } from 'react-i18next';
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 { CommonHeader } from '~/components/layout/common-header';
import { languages } from '~/tools/language';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
import { manageNamespaces } from '~/tools/server/translation-namespaces';
@@ -20,9 +20,9 @@ const PreferencesPage = ({ locale }: InferGetServerSidePropsType<typeof getServe
return (
<ManageLayout>
<CommonHeader>
<Head>
<title>Preferences Homarr</title>
</CommonHeader>
</Head>
<Title mb="xl">Preferences</Title>
{data && <SettingsComponent settings={data.settings} />}
@@ -141,7 +141,12 @@ const SelectItem = forwardRef<HTMLDivElement, ItemProps>(
);
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 {
props: {
...translations,