✨ Add new config format
This commit is contained in:
@@ -1,86 +0,0 @@
|
||||
import { TextInput, Button, Stack, Textarea } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { ColorSelector } from './ColorSelector';
|
||||
import { OpacitySelector } from './OpacitySelector';
|
||||
import { AppCardWidthSelector } from './AppCardWidthSelector';
|
||||
import { ShadeSelector } from './ShadeSelector';
|
||||
import { GrowthSelector } from './GrowthSelector';
|
||||
|
||||
export default function TitleChanger() {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
title: config.settings.title,
|
||||
logo: config.settings.logo,
|
||||
favicon: config.settings.favicon,
|
||||
background: config.settings.background,
|
||||
customCSS: config.settings.customCSS,
|
||||
},
|
||||
});
|
||||
|
||||
const saveChanges = (values: {
|
||||
title?: string;
|
||||
logo?: string;
|
||||
favicon?: string;
|
||||
background?: string;
|
||||
customCSS?: string;
|
||||
}) => {
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
title: values.title,
|
||||
logo: values.logo,
|
||||
favicon: values.favicon,
|
||||
background: values.background,
|
||||
customCSS: values.customCSS,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack mb="md" mr="sm" mt="xs">
|
||||
<form onSubmit={form.onSubmit((values) => saveChanges(values))}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={t('pageTitle.label')}
|
||||
placeholder="Homarr 🦞"
|
||||
{...form.getInputProps('title')}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('logo.label')}
|
||||
placeholder="/imgs/logo.png"
|
||||
{...form.getInputProps('logo')}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('favicon.label')}
|
||||
placeholder="/imgs/favicon/favicon.png"
|
||||
{...form.getInputProps('favicon')}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('background.label')}
|
||||
placeholder="/img/background.png"
|
||||
{...form.getInputProps('background')}
|
||||
/>
|
||||
<Textarea
|
||||
minRows={5}
|
||||
label={t('customCSS.label')}
|
||||
placeholder={t('customCSS.placeholder')}
|
||||
{...form.getInputProps('customCSS')}
|
||||
/>
|
||||
<Button type="submit">{t('buttons.submit')}</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
<GrowthSelector />
|
||||
<ColorSelector type="primary" />
|
||||
<ColorSelector type="secondary" />
|
||||
<ShadeSelector />
|
||||
<OpacitySelector />
|
||||
<AppCardWidthSelector />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Text, Slider, Stack } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
export function AppCardWidthSelector() {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { t } = useTranslation('settings/customization/app-width');
|
||||
|
||||
const setappCardWidth = (appCardWidth: number) => {
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
appCardWidth,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Text>{t('label')}</Text>
|
||||
<Slider
|
||||
label={config.settings.appCardWidth?.toFixed(1)}
|
||||
defaultValue={config.settings.appCardWidth ?? 0.7}
|
||||
step={0.1}
|
||||
min={0.3}
|
||||
max={1.2}
|
||||
styles={{ markLabel: { fontSize: 'xx-small' } }}
|
||||
onChange={(value) => setappCardWidth(value)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ColorSwatch, Grid, Group, Popover, Text, useMantineTheme } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { useColorTheme } from '../../tools/color';
|
||||
|
||||
interface ColorControlProps {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export function ColorSelector({ type }: ColorControlProps) {
|
||||
const { config, setConfig } = useConfig();
|
||||
const [opened, setOpened] = useState(false);
|
||||
const { primaryColor, secondaryColor, setPrimaryColor, setSecondaryColor } = useColorTheme();
|
||||
const { t } = useTranslation('settings/customization/color-selector');
|
||||
|
||||
const theme = useMantineTheme();
|
||||
const colors = Object.keys(theme.colors).map((color) => ({
|
||||
swatch: theme.colors[color][6],
|
||||
color,
|
||||
}));
|
||||
|
||||
const configColor = type === 'primary' ? primaryColor : secondaryColor;
|
||||
|
||||
const setConfigColor = (color: string) => {
|
||||
if (type === 'primary') {
|
||||
setPrimaryColor(color);
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
primaryColor: color,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setSecondaryColor(color);
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
secondaryColor: color,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const swatches = colors.map(({ color, swatch }) => (
|
||||
<Grid.Col span={2} key={color}>
|
||||
<ColorSwatch
|
||||
component="button"
|
||||
type="button"
|
||||
onClick={() => setConfigColor(color)}
|
||||
color={swatch}
|
||||
size={22}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</Grid.Col>
|
||||
));
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Popover
|
||||
width={250}
|
||||
withinPortal
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
position="left"
|
||||
withArrow
|
||||
>
|
||||
<Popover.Target>
|
||||
<ColorSwatch
|
||||
component="button"
|
||||
type="button"
|
||||
color={theme.colors[configColor][6]}
|
||||
onClick={() => setOpened((o) => !o)}
|
||||
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>
|
||||
);
|
||||
}
|
||||
102
src/components/Settings/Common/ConfigActions.tsx
Normal file
102
src/components/Settings/Common/ConfigActions.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Button, Center, Group } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconCheck, IconDownload, IconPlus, IconTrash, IconX } from '@tabler/icons';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import fileDownload from 'js-file-download';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import Tip from '../../layout/Tip';
|
||||
import { CreateConfigCopyModal } from './ConfigActions/CreateCopyModal';
|
||||
|
||||
export default function ConfigActions() {
|
||||
const { t } = useTranslation(['settings/general/config-changer', 'settings/common']);
|
||||
const [createCopyModalOpened, createCopyModal] = useDisclosure(false);
|
||||
const { config } = useConfigContext();
|
||||
const { mutateAsync } = useDeleteConfigMutation(config?.configProperties.name ?? 'default');
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
const handleDownload = () => {
|
||||
// TODO: remove secrets
|
||||
fileDownload(JSON.stringify(config, null, '\t'), `${config?.configProperties.name}.json`);
|
||||
};
|
||||
|
||||
const handleDeletion = async () => {
|
||||
await mutateAsync();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateConfigCopyModal
|
||||
opened={createCopyModalOpened}
|
||||
closeModal={createCopyModal.close}
|
||||
initialConfigName={config.configProperties.name}
|
||||
/>
|
||||
<Group spacing="xs" position="center">
|
||||
<Button
|
||||
size="xs"
|
||||
leftIcon={<IconDownload size={18} />}
|
||||
variant="default"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
{t('buttons.download')}
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
leftIcon={<IconTrash size={18} />}
|
||||
variant="default"
|
||||
onClick={handleDeletion}
|
||||
>
|
||||
{t('buttons.delete.text')}
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
leftIcon={<IconPlus size={18} />}
|
||||
variant="default"
|
||||
onClick={createCopyModal.open}
|
||||
>
|
||||
{t('buttons.saveCopy')}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Center>
|
||||
<Tip>{t('settings/common:tips.configTip')}</Tip>
|
||||
</Center>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const useDeleteConfigMutation = (configName: string) => {
|
||||
const { t } = useTranslation(['settings/general/config-changer']);
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['config/delete', { configName }],
|
||||
mutationFn: () => fetchDeletion(configName),
|
||||
onSuccess() {
|
||||
showNotification({
|
||||
title: t('buttons.delete.notifications.deleted.title'),
|
||||
icon: <IconCheck />,
|
||||
color: 'green',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: t('buttons.delete.notifications.deleted.message'),
|
||||
});
|
||||
// TODO: set config to default config and use fallback config if necessary
|
||||
},
|
||||
onError() {
|
||||
showNotification({
|
||||
title: t('buttons.delete.notifications.deleteFailed.title'),
|
||||
icon: <IconX />,
|
||||
color: 'red',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: t('buttons.delete.notifications.deleteFailed.message'),
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const fetchDeletion = async (configName: string) => {
|
||||
return await (await fetch(`/api/configs/${configName}`)).json();
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Button, Group, Modal, TextInput, Title } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconCheck } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
|
||||
interface CreateConfigCopyModalProps {
|
||||
opened: boolean;
|
||||
closeModal: () => void;
|
||||
initialConfigName: string;
|
||||
}
|
||||
|
||||
export const CreateConfigCopyModal = ({
|
||||
opened,
|
||||
closeModal,
|
||||
initialConfigName,
|
||||
}: CreateConfigCopyModalProps) => {
|
||||
const { t } = useTranslation(['settings/general/config-changer']);
|
||||
const { config } = useConfigContext();
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
configName: initialConfigName,
|
||||
},
|
||||
validate: {
|
||||
configName: (v) => (!v ? t('modal.form.configName.validation.required') : null),
|
||||
},
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
form.setFieldValue('configName', initialConfigName);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const handleSubmit = (values: typeof form.values) => {
|
||||
if (!form.isValid) return;
|
||||
// TODO: create config file with copied data
|
||||
closeModal();
|
||||
showNotification({
|
||||
title: t('modal.events.configSaved.title'),
|
||||
icon: <IconCheck />,
|
||||
color: 'green',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: t('modal.events.configSaved.message', { configName: values.configName }),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
radius="md"
|
||||
opened={opened}
|
||||
onClose={handleClose}
|
||||
title={<Title order={4}>{t('modal.title')}</Title>}
|
||||
>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<TextInput
|
||||
label={t('modal.form.configName.label')}
|
||||
placeholder={t('modal.form.configName.placeholder')}
|
||||
{...form.getInputProps('configName')}
|
||||
/>
|
||||
<Group position="right" mt="md">
|
||||
<Button type="submit">{t('modal.form.submitButton')}</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -5,9 +5,9 @@ import { forwardRef, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getCookie, setCookie } from 'cookies-next';
|
||||
import { getLanguageByCode, Language } from '../../tools/language';
|
||||
import { getLanguageByCode, Language } from '../../../tools/language';
|
||||
|
||||
export default function LanguageSwitch() {
|
||||
export default function LanguageSelect() {
|
||||
const { t, i18n } = useTranslation('settings/general/internationalization');
|
||||
const { changeLanguage } = i18n;
|
||||
const configLocale = getCookie('config-locale');
|
||||
44
src/components/Settings/Common/SearchEngineEnabledSwitch.tsx
Normal file
44
src/components/Settings/Common/SearchEngineEnabledSwitch.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Switch } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
import { SearchEngineCommonSettingsType } from '../../../types/settings';
|
||||
|
||||
interface SearchEnabledSwitchProps {
|
||||
defaultValue: boolean | undefined;
|
||||
}
|
||||
|
||||
export function SearchEnabledSwitch({ defaultValue }: SearchEnabledSwitchProps) {
|
||||
const { t } = useTranslation('settings/general/search-engine');
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
const [enabled, setEnabled] = useState<boolean>(defaultValue ?? true);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const toggleEnabled = () => {
|
||||
setEnabled(!enabled);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
common: {
|
||||
...prev.settings.common,
|
||||
searchEngine: {
|
||||
...prev.settings.common.searchEngine,
|
||||
properties: {
|
||||
...prev.settings.common.searchEngine.properties,
|
||||
enabled: !enabled,
|
||||
},
|
||||
} as SearchEngineCommonSettingsType,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch checked={enabled} onChange={toggleEnabled} size="md" label={t('searchEnabled.label')} />
|
||||
);
|
||||
}
|
||||
126
src/components/Settings/Common/SearchEngineSelector.tsx
Normal file
126
src/components/Settings/Common/SearchEngineSelector.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Alert, Paper, SegmentedControl, Stack, TextInput, Title } from '@mantine/core';
|
||||
import { IconInfoCircle } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
import {
|
||||
CommonSearchEngineCommonSettingsType,
|
||||
SearchEngineCommonSettingsType,
|
||||
} from '../../../types/settings';
|
||||
import Tip from '../../layout/Tip';
|
||||
|
||||
interface Props {
|
||||
searchEngine: SearchEngineCommonSettingsType;
|
||||
}
|
||||
|
||||
// TODO: discuss with @manuel-rw the design of the search engine
|
||||
export const SearchEngineSelector = ({ searchEngine }: Props) => {
|
||||
const { t } = useTranslation(['settings/general/search-engine']);
|
||||
const { updateSearchEngineConfig } = useUpdateSearchEngineConfig();
|
||||
|
||||
const [engine, setEngine] = useState(searchEngine.type);
|
||||
const [searchUrl, setSearchUrl] = useState(
|
||||
searchEngine.type === 'custom' ? searchEngine.properties.template : searchUrls.google
|
||||
);
|
||||
|
||||
const onEngineChange = (value: EngineType) => {
|
||||
setEngine(value);
|
||||
updateSearchEngineConfig(value, searchUrl);
|
||||
};
|
||||
|
||||
const onSearchUrlChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const url = ev.currentTarget.value;
|
||||
setSearchUrl(url);
|
||||
updateSearchEngineConfig(engine, url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={0} mt="xs">
|
||||
<Title order={5} mb="xs">
|
||||
{t('title')}
|
||||
</Title>
|
||||
|
||||
<SegmentedControl
|
||||
fullWidth
|
||||
mb="sm"
|
||||
title={t('title')}
|
||||
value={engine}
|
||||
onChange={onEngineChange}
|
||||
data={searchEngineOptions}
|
||||
/>
|
||||
{engine === 'custom' && (
|
||||
<Paper p="md" py="sm" mb="xs" withBorder>
|
||||
<Title order={6}>{t('customEngine.title')}</Title>
|
||||
<Tip>{t('tips.placeholderTip')}</Tip>
|
||||
<TextInput
|
||||
label={t('customEngine.label')}
|
||||
placeholder={t('customEngine.placeholder')}
|
||||
value={searchUrl}
|
||||
onChange={onSearchUrlChange}
|
||||
/>
|
||||
</Paper>
|
||||
)}
|
||||
<Alert icon={<IconInfoCircle />} color="blue">
|
||||
{t('tips.generalTip')}
|
||||
</Alert>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const searchEngineOptions: { label: string; value: EngineType }[] = [
|
||||
{ label: 'Google', value: 'google' },
|
||||
{ label: 'DuckDuckGo', value: 'duckDuckGo' },
|
||||
{ label: 'Bing', value: 'bing' },
|
||||
{ label: 'Custom', value: 'custom' },
|
||||
];
|
||||
|
||||
export const searchUrls: { [key in CommonSearchEngineCommonSettingsType['type']]: string } = {
|
||||
google: 'https://google.com/search?q=',
|
||||
duckDuckGo: 'https://duckduckgo.com/?q=',
|
||||
bing: 'https://bing.com/search?q=',
|
||||
};
|
||||
|
||||
type EngineType = SearchEngineCommonSettingsType['type'];
|
||||
|
||||
const useUpdateSearchEngineConfig = () => {
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
if (!configName)
|
||||
return {
|
||||
updateSearchEngineConfig: () => {},
|
||||
};
|
||||
|
||||
const updateSearchEngineConfig = (engine: EngineType, searchUrl: string) => {
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
common: {
|
||||
...prev.settings.common,
|
||||
searchEngine:
|
||||
engine === 'custom'
|
||||
? {
|
||||
type: engine,
|
||||
properties: {
|
||||
...prev.settings.common.searchEngine.properties,
|
||||
template: searchUrl,
|
||||
},
|
||||
}
|
||||
: {
|
||||
type: engine,
|
||||
properties: {
|
||||
openInNewTab: prev.settings.common.searchEngine.properties.openInNewTab,
|
||||
enabled: prev.settings.common.searchEngine.properties.enabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
updateSearchEngineConfig,
|
||||
};
|
||||
};
|
||||
49
src/components/Settings/Common/SearchNewTabSwitch.tsx
Normal file
49
src/components/Settings/Common/SearchNewTabSwitch.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Switch } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
import { SearchEngineCommonSettingsType } from '../../../types/settings';
|
||||
|
||||
interface SearchNewTabSwitchProps {
|
||||
defaultValue: boolean | undefined;
|
||||
}
|
||||
|
||||
export function SearchNewTabSwitch({ defaultValue }: SearchNewTabSwitchProps) {
|
||||
const { t } = useTranslation('settings/general/search-engine');
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
const [openInNewTab, setOpenInNewTab] = useState<boolean>(defaultValue ?? true);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const toggleOpenInNewTab = () => {
|
||||
setOpenInNewTab(!openInNewTab);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
common: {
|
||||
...prev.settings.common,
|
||||
searchEngine: {
|
||||
...prev.settings.common.searchEngine,
|
||||
properties: {
|
||||
...prev.settings.common.searchEngine.properties,
|
||||
openInNewTab: !openInNewTab,
|
||||
},
|
||||
} as SearchEngineCommonSettingsType,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={openInNewTab}
|
||||
onChange={toggleOpenInNewTab}
|
||||
size="md"
|
||||
label={t('searchNewTab.label')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,89 +1,34 @@
|
||||
import { Text, SegmentedControl, TextInput, Stack } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { SearchNewTabSwitch } from '../SearchNewTabSwitch/SearchNewTabSwitch';
|
||||
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
|
||||
import { WidgetsPositionSwitch } from '../WidgetsPositionSwitch/WidgetsPositionSwitch';
|
||||
import { Space, Stack, Text } from '@mantine/core';
|
||||
import { useConfigContext } from '../../config/provider';
|
||||
import ConfigChanger from '../Config/ConfigChanger';
|
||||
import SaveConfigComponent from '../Config/SaveConfig';
|
||||
import ModuleEnabler from './ModuleEnabler';
|
||||
import Tip from '../layout/Tip';
|
||||
import LanguageSwitch from './LanguageSwitch';
|
||||
import ConfigActions from './Common/ConfigActions';
|
||||
import LanguageSelect from './Common/LanguageSelect';
|
||||
import { SearchEnabledSwitch } from './Common/SearchEngineEnabledSwitch';
|
||||
import { SearchEngineSelector } from './Common/SearchEngineSelector';
|
||||
import { SearchNewTabSwitch } from './Common/SearchNewTabSwitch';
|
||||
|
||||
export default function CommonSettings(args: any) {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { t } = useTranslation(['settings/general/search-engine', 'settings/common']);
|
||||
export default function CommonSettings() {
|
||||
const { config } = useConfigContext();
|
||||
|
||||
const matches = [
|
||||
{ label: 'Google', value: 'https://google.com/search?q=' },
|
||||
{ label: 'DuckDuckGo', value: 'https://duckduckgo.com/?q=' },
|
||||
{ label: 'Bing', value: 'https://bing.com/search?q=' },
|
||||
{ label: 'Custom', value: 'Custom' },
|
||||
];
|
||||
|
||||
const [customSearchUrl, setCustomSearchUrl] = useState(config.settings.searchUrl);
|
||||
const [searchUrl, setSearchUrl] = useState(
|
||||
matches.find((match) => match.value === config.settings.searchUrl)?.value ?? 'Custom'
|
||||
);
|
||||
if (!config) {
|
||||
return (
|
||||
<Text color="red" align="center">
|
||||
No active config
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack mb="md" mr="sm">
|
||||
<Stack spacing={0} mt="xs">
|
||||
<Text>{t('title')}</Text>
|
||||
<Tip>{t('tips.generalTip')}</Tip>
|
||||
<SegmentedControl
|
||||
fullWidth
|
||||
mb="sm"
|
||||
title={t('title')}
|
||||
value={
|
||||
// Match config.settings.searchUrl with a key in the matches array
|
||||
searchUrl
|
||||
}
|
||||
onChange={
|
||||
// Set config.settings.searchUrl to the value of the selected item
|
||||
(e) => {
|
||||
setSearchUrl(e);
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
searchUrl: e,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
data={matches}
|
||||
/>
|
||||
{searchUrl === 'Custom' && (
|
||||
<>
|
||||
<Tip>{t('tips.placeholderTip')}</Tip>
|
||||
<TextInput
|
||||
label={t('customEngine.label')}
|
||||
placeholder={t('customEngine.placeholder')}
|
||||
value={customSearchUrl}
|
||||
onChange={(event) => {
|
||||
setCustomSearchUrl(event.currentTarget.value);
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
searchUrl: event.currentTarget.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<SearchNewTabSwitch />
|
||||
<ColorSchemeSwitch />
|
||||
<WidgetsPositionSwitch />
|
||||
<ModuleEnabler />
|
||||
<LanguageSwitch />
|
||||
<SearchEngineSelector searchEngine={config.settings.common.searchEngine} />
|
||||
<SearchNewTabSwitch
|
||||
defaultValue={config.settings.common.searchEngine.properties.openInNewTab}
|
||||
/>
|
||||
<SearchEnabledSwitch defaultValue={config.settings.common.searchEngine.properties.enabled} />
|
||||
<Space />
|
||||
<LanguageSelect />
|
||||
<ConfigChanger />
|
||||
<SaveConfigComponent />
|
||||
<Tip>{t('settings/common:tips.configTip')}</Tip>
|
||||
<ConfigActions />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { CURRENT_VERSION } from '../../../data/constants';
|
||||
|
||||
export default function Credits(props: any) {
|
||||
export default function Credits() {
|
||||
const { t } = useTranslation('settings/common');
|
||||
|
||||
return (
|
||||
|
||||
43
src/components/Settings/Customization/BackgroundChanger.tsx
Normal file
43
src/components/Settings/Customization/BackgroundChanger.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { TextInput } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
|
||||
interface BackgroundChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const BackgroundChanger = ({ defaultValue }: BackgroundChangerProps) => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [backgroundImageUrl, setBackgroundImageUrl] = useState(defaultValue);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const value = ev.currentTarget.value;
|
||||
const backgroundImageUrl = value.trim().length === 0 ? undefined : value;
|
||||
setBackgroundImageUrl(backgroundImageUrl);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
backgroundImageUrl,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
label={t('background.label')}
|
||||
placeholder="/imgs/background.png"
|
||||
value={backgroundImageUrl}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
104
src/components/Settings/Customization/ColorSelector.tsx
Normal file
104
src/components/Settings/Customization/ColorSelector.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
ColorSwatch,
|
||||
Grid,
|
||||
Group,
|
||||
MantineTheme,
|
||||
Popover,
|
||||
Text,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useColorTheme } from '../../../tools/color';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
|
||||
interface ColorControlProps {
|
||||
defaultValue: MantineTheme['primaryColor'] | undefined;
|
||||
type: 'primary' | 'secondary';
|
||||
}
|
||||
|
||||
export function ColorSelector({ type, defaultValue }: ColorControlProps) {
|
||||
const { t } = useTranslation('settings/customization/color-selector');
|
||||
const [color, setColor] = useState(defaultValue);
|
||||
const [popoverOpened, popover] = useDisclosure(false);
|
||||
const { setPrimaryColor, setSecondaryColor } = useColorTheme();
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
const theme = useMantineTheme();
|
||||
const colors = Object.keys(theme.colors).map((color) => ({
|
||||
swatch: theme.colors[color][6],
|
||||
color,
|
||||
}));
|
||||
|
||||
if (!color || !configName) return null;
|
||||
|
||||
const handleSelection = (color: MantineTheme['primaryColor']) => {
|
||||
setColor(color);
|
||||
if (type === 'primary') setPrimaryColor(color);
|
||||
else setSecondaryColor(color);
|
||||
updateConfig(configName, (prev) => {
|
||||
const colors = prev.settings.customization.colors;
|
||||
colors[type] = color;
|
||||
return {
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
colors,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const swatches = colors.map(({ color, swatch }) => (
|
||||
<Grid.Col span={2} key={color}>
|
||||
<ColorSwatch
|
||||
component="button"
|
||||
type="button"
|
||||
onClick={() => handleSelection(color)}
|
||||
color={swatch}
|
||||
size={22}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</Grid.Col>
|
||||
));
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Popover
|
||||
width={250}
|
||||
withinPortal
|
||||
opened={popoverOpened}
|
||||
onClose={popover.close}
|
||||
position="left"
|
||||
withArrow
|
||||
>
|
||||
<Popover.Target>
|
||||
<ColorSwatch
|
||||
component="button"
|
||||
type="button"
|
||||
color={theme.colors[color][6]}
|
||||
onClick={popover.toggle}
|
||||
size={22}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Grid gutter="lg" columns={14}>
|
||||
{swatches}
|
||||
</Grid>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
<Text>
|
||||
{t('suffix', {
|
||||
color: type[0].toUpperCase() + type.slice(1),
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
44
src/components/Settings/Customization/CustomCssChanger.tsx
Normal file
44
src/components/Settings/Customization/CustomCssChanger.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Textarea } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
|
||||
interface CustomCssChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const CustomCssChanger = ({ defaultValue }: CustomCssChangerProps) => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [customCss, setCustomCss] = useState(defaultValue);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = (ev) => {
|
||||
const value = ev.currentTarget.value;
|
||||
const customCss = value.trim().length === 0 ? undefined : value;
|
||||
setCustomCss(customCss);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
customCss,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
minRows={5}
|
||||
label={t('customCSS.label')}
|
||||
placeholder={t('customCSS.placeholder')}
|
||||
value={customCss}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
43
src/components/Settings/Customization/FaviconChanger.tsx
Normal file
43
src/components/Settings/Customization/FaviconChanger.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { TextInput } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
|
||||
interface FaviconChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const FaviconChanger = ({ defaultValue }: FaviconChangerProps) => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [faviconUrl, setFaviconUrl] = useState(defaultValue);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const value = ev.currentTarget.value;
|
||||
const faviconUrl = value.trim().length === 0 ? undefined : value;
|
||||
setFaviconUrl(faviconUrl);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
faviconUrl,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
label={t('favicon.label')}
|
||||
placeholder="/imgs/favicon/favicon.svg"
|
||||
value={faviconUrl}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
164
src/components/Settings/Customization/LayoutSelector.tsx
Normal file
164
src/components/Settings/Customization/LayoutSelector.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import {
|
||||
Box,
|
||||
Center,
|
||||
Checkbox,
|
||||
createStyles,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconBrandDocker, IconLayout, IconSearch } from '@tabler/icons';
|
||||
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
import { CustomizationSettingsType } from '../../../types/settings';
|
||||
import { Logo } from '../../layout/Logo';
|
||||
|
||||
interface LayoutSelectorProps {
|
||||
defaultLayout: CustomizationSettingsType['layout'] | undefined;
|
||||
}
|
||||
|
||||
// TODO: add translations
|
||||
export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
const [leftSidebar, setLeftSidebar] = useState(defaultLayout?.enabledLeftSidebar ?? true);
|
||||
const [rightSidebar, setRightSidebar] = useState(defaultLayout?.enabledRightSidebar ?? true);
|
||||
const [docker, setDocker] = useState(defaultLayout?.enabledDocker ?? false);
|
||||
const [ping, setPing] = useState(defaultLayout?.enabledPing ?? false);
|
||||
const [searchBar, setSearchBar] = useState(defaultLayout?.enabledSearchbar ?? false);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange = (
|
||||
key: keyof CustomizationSettingsType['layout'],
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
setState: Dispatch<SetStateAction<boolean>>
|
||||
) => {
|
||||
const value = event.target.checked;
|
||||
setState(value);
|
||||
updateConfig(configName, (prev) => {
|
||||
const layout = prev.settings.customization.layout;
|
||||
|
||||
layout[key] = value;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
layout,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={classes.box} p="xl" pb="sm">
|
||||
<Stack spacing="xs">
|
||||
<Group spacing={5}>
|
||||
<IconLayout size={20} />
|
||||
<Title order={6}>Dashboard layout</Title>
|
||||
</Group>
|
||||
|
||||
<Text color="dimmed" size="sm">
|
||||
You can adjust the layout of the Dashboard to your preferences. The main are cannot be
|
||||
turned on or off
|
||||
</Text>
|
||||
|
||||
<Paper px="xs" py={2} withBorder>
|
||||
<Group position="apart">
|
||||
<Logo size="xs" />
|
||||
<Group spacing={4}>
|
||||
{searchBar ? (
|
||||
<Paper withBorder p={2} w={60}>
|
||||
<Group spacing={2} align="center">
|
||||
<IconSearch size={8} />
|
||||
<Text size={8} color="dimmed">
|
||||
Search
|
||||
</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
) : null}
|
||||
{docker ? <IconBrandDocker size={18} color="#0db7ed" /> : null}
|
||||
</Group>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Group align="stretch">
|
||||
{leftSidebar && (
|
||||
<Paper p="xs" withBorder>
|
||||
<Center style={{ height: '100%' }}>
|
||||
<Text align="center">Sidebar</Text>
|
||||
</Center>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Paper className={classes.main} p="xs" withBorder>
|
||||
<Text align="center">Main</Text>
|
||||
<Text color="dimmed" size="xs" align="center">
|
||||
Can be used for categories,
|
||||
<br />
|
||||
services and integrations
|
||||
</Text>
|
||||
</Paper>
|
||||
|
||||
{rightSidebar && (
|
||||
<Paper p="xs" withBorder>
|
||||
<Center style={{ height: '100%' }}>
|
||||
<Text align="center">Sidebar</Text>
|
||||
</Center>
|
||||
</Paper>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Stack spacing="xs">
|
||||
<Checkbox
|
||||
label="Enable left sidebar"
|
||||
description="Optional. Can be used for services and integrations only"
|
||||
checked={leftSidebar}
|
||||
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Enable right sidebar"
|
||||
description="Optional. Can be used for services and integrations only"
|
||||
checked={rightSidebar}
|
||||
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Enable search bar"
|
||||
checked={searchBar}
|
||||
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Enable docker"
|
||||
checked={docker}
|
||||
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Enable pings"
|
||||
checked={ping}
|
||||
onChange={(ev) => handleChange('enabledPing', ev, setPing)}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
main: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
box: {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1],
|
||||
borderRadius: theme.radius.md,
|
||||
},
|
||||
}));
|
||||
43
src/components/Settings/Customization/LogoImageChanger.tsx
Normal file
43
src/components/Settings/Customization/LogoImageChanger.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { TextInput } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
|
||||
interface LogoImageChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const LogoImageChanger = ({ defaultValue }: LogoImageChangerProps) => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [logoImageSrc, setLogoImageSrc] = useState(defaultValue);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const value = ev.currentTarget.value;
|
||||
const logoImageSrc = value.trim().length === 0 ? undefined : value;
|
||||
setLogoImageSrc(logoImageSrc);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
logoImageUrl: logoImageSrc,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
label={t('logo.label')}
|
||||
placeholder="/imgs/logo/logo.png"
|
||||
value={logoImageSrc}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
43
src/components/Settings/Customization/MetaTitleChanger.tsx
Normal file
43
src/components/Settings/Customization/MetaTitleChanger.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { TextInput } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
|
||||
interface MetaTitleChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const MetaTitleChanger = ({ defaultValue }: MetaTitleChangerProps) => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [metaTitle, setMetaTitle] = useState(defaultValue);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const value = ev.currentTarget.value;
|
||||
const metaTitle = value.trim().length === 0 ? undefined : value;
|
||||
setMetaTitle(metaTitle);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
metaTitle,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
label={t('metaTitle.label')}
|
||||
placeholder={t('metaTitle.placeholder')}
|
||||
value={metaTitle}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
60
src/components/Settings/Customization/OpacitySelector.tsx
Normal file
60
src/components/Settings/Customization/OpacitySelector.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Text, Slider, Stack } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
|
||||
interface OpacitySelectorProps {
|
||||
defaultValue: number | undefined;
|
||||
}
|
||||
|
||||
export function OpacitySelector({ defaultValue }: OpacitySelectorProps) {
|
||||
const [opacity, setOpacity] = useState(defaultValue || 100);
|
||||
const { t } = useTranslation('settings/customization/opacity-selector');
|
||||
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange = (opacity: number) => {
|
||||
setOpacity(opacity);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
appOpacity: opacity,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Text>{t('label')}</Text>
|
||||
<Slider
|
||||
defaultValue={opacity}
|
||||
step={10}
|
||||
min={10}
|
||||
marks={MARKS}
|
||||
styles={{ markLabel: { fontSize: 'xx-small' } }}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const MARKS = [
|
||||
{ value: 10, label: '10' },
|
||||
{ value: 20, label: '20' },
|
||||
{ value: 30, label: '30' },
|
||||
{ value: 40, label: '40' },
|
||||
{ value: 50, label: '50' },
|
||||
{ value: 60, label: '60' },
|
||||
{ value: 70, label: '70' },
|
||||
{ value: 80, label: '80' },
|
||||
{ value: 90, label: '90' },
|
||||
{ value: 100, label: '100' },
|
||||
];
|
||||
43
src/components/Settings/Customization/PageTitleChanger.tsx
Normal file
43
src/components/Settings/Customization/PageTitleChanger.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { TextInput } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
|
||||
interface PageTitleChangerProps {
|
||||
defaultValue: string | undefined;
|
||||
}
|
||||
|
||||
export const PageTitleChanger = ({ defaultValue }: PageTitleChangerProps) => {
|
||||
const { t } = useTranslation('settings/customization/page-appearance');
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
const { name: configName } = useConfigContext();
|
||||
const [pageTitle, setPageTitle] = useState(defaultValue);
|
||||
|
||||
if (!configName) return null;
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
||||
const value = ev.currentTarget.value;
|
||||
const pageTitle = value.trim().length === 0 ? undefined : value;
|
||||
setPageTitle(pageTitle);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
pageTitle,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
label={t('pageTitle.label')}
|
||||
placeholder={t('pageTitle.placeholder')}
|
||||
value={pageTitle}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -10,35 +10,48 @@ import {
|
||||
Grid,
|
||||
} from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { useColorTheme } from '../../tools/color';
|
||||
import { useColorTheme } from '../../../tools/color';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { useConfigStore } from '../../../config/store';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
|
||||
export function ShadeSelector() {
|
||||
const { config, setConfig } = useConfig();
|
||||
const [opened, setOpened] = useState(false);
|
||||
interface ShadeSelectorProps {
|
||||
defaultValue: MantineTheme['primaryShade'] | undefined;
|
||||
}
|
||||
|
||||
export function ShadeSelector({ defaultValue }: ShadeSelectorProps) {
|
||||
const { t } = useTranslation('settings/customization/shade-selector');
|
||||
const [shade, setShade] = useState(defaultValue);
|
||||
const [popoverOpened, popover] = useDisclosure(false);
|
||||
const { primaryColor, setPrimaryShade } = useColorTheme();
|
||||
|
||||
const { primaryColor, secondaryColor, primaryShade, setPrimaryShade } = useColorTheme();
|
||||
const { name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
|
||||
const theme = useMantineTheme();
|
||||
const primaryShades = theme.colors[primaryColor].map((s, i) => ({
|
||||
swatch: theme.colors[primaryColor][i],
|
||||
shade: i as MantineTheme['primaryShade'],
|
||||
}));
|
||||
const secondaryShades = theme.colors[secondaryColor].map((s, i) => ({
|
||||
swatch: theme.colors[secondaryColor][i],
|
||||
shade: i as MantineTheme['primaryShade'],
|
||||
}));
|
||||
|
||||
const setConfigShade = (shade: MantineTheme['primaryShade']) => {
|
||||
if (shade === undefined || !configName) return null;
|
||||
|
||||
const handleSelection = (shade: MantineTheme['primaryShade']) => {
|
||||
setPrimaryShade(shade);
|
||||
setConfig({
|
||||
...config,
|
||||
setShade(shade);
|
||||
updateConfig(configName, (prev) => ({
|
||||
...prev,
|
||||
settings: {
|
||||
...config.settings,
|
||||
primaryShade: shade,
|
||||
...prev.settings,
|
||||
customization: {
|
||||
...prev.settings.customization,
|
||||
colors: {
|
||||
...prev.settings.customization.colors,
|
||||
shade,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
const primarySwatches = primaryShades.map(({ swatch, shade }) => (
|
||||
@@ -46,20 +59,7 @@ export function ShadeSelector() {
|
||||
<ColorSwatch
|
||||
component="button"
|
||||
type="button"
|
||||
onClick={() => setConfigShade(shade)}
|
||||
color={swatch}
|
||||
size={22}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</Grid.Col>
|
||||
));
|
||||
|
||||
const secondarySwatches = secondaryShades.map(({ swatch, shade }) => (
|
||||
<Grid.Col span={1} key={Number(shade)}>
|
||||
<ColorSwatch
|
||||
component="button"
|
||||
type="button"
|
||||
onClick={() => setConfigShade(shade)}
|
||||
onClick={() => handleSelection(shade)}
|
||||
color={swatch}
|
||||
size={22}
|
||||
style={{ cursor: 'pointer' }}
|
||||
@@ -72,8 +72,8 @@ export function ShadeSelector() {
|
||||
<Popover
|
||||
width={350}
|
||||
withinPortal
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
opened={popoverOpened}
|
||||
onClose={popover.close}
|
||||
position="left"
|
||||
withArrow
|
||||
>
|
||||
@@ -81,8 +81,8 @@ export function ShadeSelector() {
|
||||
<ColorSwatch
|
||||
component="button"
|
||||
type="button"
|
||||
color={theme.colors[primaryColor][Number(primaryShade)]}
|
||||
onClick={() => setOpened((o) => !o)}
|
||||
color={theme.colors[primaryColor][Number(shade)]}
|
||||
onClick={popover.toggle}
|
||||
size={22}
|
||||
style={{ display: 'block', cursor: 'pointer' }}
|
||||
/>
|
||||
@@ -91,7 +91,6 @@ export function ShadeSelector() {
|
||||
<Stack spacing="xs">
|
||||
<Grid gutter="lg" columns={10}>
|
||||
{primarySwatches}
|
||||
{secondarySwatches}
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Popover.Dropdown>
|
||||
35
src/components/Settings/CustomizationSettings.tsx
Normal file
35
src/components/Settings/CustomizationSettings.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Stack } from '@mantine/core';
|
||||
import { useConfigContext } from '../../config/provider';
|
||||
import { ColorSelector } from './Customization/ColorSelector';
|
||||
import { BackgroundChanger } from './Customization/BackgroundChanger';
|
||||
import { CustomCssChanger } from './Customization/CustomCssChanger';
|
||||
import { FaviconChanger } from './Customization/FaviconChanger';
|
||||
import { LogoImageChanger } from './Customization/LogoImageChanger';
|
||||
import { MetaTitleChanger } from './Customization/MetaTitleChanger';
|
||||
import { PageTitleChanger } from './Customization/PageTitleChanger';
|
||||
import { OpacitySelector } from './Customization/OpacitySelector';
|
||||
import { ShadeSelector } from './Customization/ShadeSelector';
|
||||
import { LayoutSelector } from './Customization/LayoutSelector';
|
||||
|
||||
export default function CustomizationSettings() {
|
||||
const { config } = useConfigContext();
|
||||
|
||||
return (
|
||||
<Stack mb="md" mr="sm" mt="xs">
|
||||
<LayoutSelector defaultLayout={config?.settings.customization.layout} />
|
||||
<PageTitleChanger defaultValue={config?.settings.customization.pageTitle} />
|
||||
<MetaTitleChanger defaultValue={config?.settings.customization.metaTitle} />
|
||||
<LogoImageChanger defaultValue={config?.settings.customization.logoImageUrl} />
|
||||
<FaviconChanger defaultValue={config?.settings.customization.faviconUrl} />
|
||||
<BackgroundChanger defaultValue={config?.settings.customization.backgroundImageUrl} />
|
||||
<CustomCssChanger defaultValue={config?.settings.customization.customCss} />
|
||||
<ColorSelector type="primary" defaultValue={config?.settings.customization.colors.primary} />
|
||||
<ColorSelector
|
||||
type="secondary"
|
||||
defaultValue={config?.settings.customization.colors.secondary}
|
||||
/>
|
||||
<ShadeSelector defaultValue={config?.settings.customization.colors.shade} />
|
||||
<OpacitySelector defaultValue={config?.settings.customization.appOpacity} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Switch } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
export function GrowthSelector() {
|
||||
const { config, setConfig } = useConfig();
|
||||
const defaultPosition = config?.settings?.grow || false;
|
||||
const [growState, setGrowState] = useState(defaultPosition);
|
||||
const { t } = useTranslation('settings/common.json');
|
||||
const toggleGrowState = () => {
|
||||
setGrowState(!growState);
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
grow: !growState,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
label={t('settings/common:grow')}
|
||||
checked={growState === true}
|
||||
onChange={() => toggleGrowState()}
|
||||
size="md"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Checkbox, HoverCard, SimpleGrid, Stack, Text, Title } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import * as Modules from '../../modules';
|
||||
import { IModule } from '../../modules/ModuleTypes';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
export default function ModuleEnabler(props: any) {
|
||||
const { t } = useTranslation('settings/general/module-enabler');
|
||||
const modules = Object.values(Modules).map((module) => module);
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={4}>{t('title')}</Title>
|
||||
<SimpleGrid cols={3} spacing="sm">
|
||||
{modules.map((module) => (
|
||||
<ModuleToggle key={module.id} module={module} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const ModuleToggle = ({ module }: { module: IModule }) => {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { t } = useTranslation(`modules/${module.id}`);
|
||||
|
||||
return (
|
||||
<HoverCard withArrow withinPortal width={200} shadow="md" openDelay={200}>
|
||||
<HoverCard.Target>
|
||||
<Checkbox
|
||||
key={module.id}
|
||||
size="md"
|
||||
checked={config.modules?.[module.id]?.enabled ?? false}
|
||||
label={t('descriptor.name', {
|
||||
defaultValue: 'Unknown',
|
||||
})}
|
||||
onChange={(e) => {
|
||||
setConfig({
|
||||
...config,
|
||||
modules: {
|
||||
...config.modules,
|
||||
[module.id]: {
|
||||
...config.modules?.[module.id],
|
||||
enabled: e.currentTarget.checked,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</HoverCard.Target>
|
||||
<HoverCard.Dropdown>
|
||||
<Title order={4}>{t('descriptor.name')}</Title>
|
||||
<Text size="sm">{t('descriptor.description')}</Text>
|
||||
</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
);
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Text, Slider, Stack } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
export function OpacitySelector() {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { t } = useTranslation('settings/customization/opacity-selector');
|
||||
|
||||
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' },
|
||||
];
|
||||
|
||||
const setConfigOpacity = (opacity: number) => {
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
appOpacity: opacity,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Text>{t('label')}</Text>
|
||||
<Slider
|
||||
defaultValue={config.settings.appOpacity || 100}
|
||||
step={10}
|
||||
min={10}
|
||||
marks={MARKS}
|
||||
styles={{ markLabel: { fontSize: 'xx-small' } }}
|
||||
onChange={(value) => setConfigOpacity(value)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -4,27 +4,27 @@ import { useState } from 'react';
|
||||
import { IconSettings } from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import AdvancedSettings from './AdvancedSettings';
|
||||
import CustomizationSettings from './CustomizationSettings';
|
||||
import CommonSettings from './CommonSettings';
|
||||
import Credits from './Credits';
|
||||
|
||||
function SettingsMenu(props: any) {
|
||||
function SettingsMenu() {
|
||||
const { t } = useTranslation('settings/common');
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="Common">
|
||||
<Tabs defaultValue="common">
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab value="Common">{t('tabs.common')}</Tabs.Tab>
|
||||
<Tabs.Tab value="Customizations">{t('tabs.customizations')}</Tabs.Tab>
|
||||
<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">
|
||||
<Tabs.Panel data-autofocus value="common">
|
||||
<ScrollArea style={{ height: '78vh' }} offsetScrollbars>
|
||||
<CommonSettings />
|
||||
</ScrollArea>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="Customizations">
|
||||
<Tabs.Panel value="customization">
|
||||
<ScrollArea style={{ height: '78vh' }} offsetScrollbars>
|
||||
<AdvancedSettings />
|
||||
<CustomizationSettings />
|
||||
</ScrollArea>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
Reference in New Issue
Block a user