mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-08 22:45:49 +01:00
✨ Add new config format
This commit is contained in:
27
package.json
27
package.json
@@ -32,27 +32,27 @@
|
||||
"@dnd-kit/utilities": "^3.2.0",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/server": "^11.10.0",
|
||||
"@mantine/carousel": "^5.1.0",
|
||||
"@mantine/core": "^5.7.2",
|
||||
"@mantine/dates": "^5.7.2",
|
||||
"@mantine/dropzone": "^5.7.2",
|
||||
"@mantine/form": "^5.7.2",
|
||||
"@mantine/hooks": "^5.7.2",
|
||||
"@mantine/modals": "^5.7.2",
|
||||
"@mantine/next": "^5.2.3",
|
||||
"@mantine/notifications": "^5.7.2",
|
||||
"@mantine/prism": "^5.0.0",
|
||||
"@mantine/carousel": "^5.9.0",
|
||||
"@mantine/core": "^5.9.0",
|
||||
"@mantine/dates": "^5.9.0",
|
||||
"@mantine/dropzone": "^5.9.0",
|
||||
"@mantine/form": "^5.9.0",
|
||||
"@mantine/hooks": "^5.9.0",
|
||||
"@mantine/modals": "^5.9.0",
|
||||
"@mantine/next": "^5.9.0",
|
||||
"@mantine/notifications": "^5.9.0",
|
||||
"@mantine/prism": "^5.9.0",
|
||||
"@nivo/core": "^0.79.0",
|
||||
"@nivo/line": "^0.79.1",
|
||||
"@tabler/icons": "^1.78.0",
|
||||
"@tabler/icons": "^1.106.0",
|
||||
"@tanstack/react-query": "^4.2.1",
|
||||
"add": "^2.0.6",
|
||||
"axios": "^0.27.2",
|
||||
"consola": "^2.15.3",
|
||||
"cookies-next": "^2.1.1",
|
||||
"dayjs": "^1.11.6",
|
||||
"dockerode": "^3.3.2",
|
||||
"embla-carousel-react": "^7.0.0",
|
||||
"fily-publish-gridstack": "^0.0.13",
|
||||
"framer-motion": "^6.5.1",
|
||||
"i18next": "^21.9.1",
|
||||
"i18next-browser-languagedetector": "^6.1.5",
|
||||
@@ -69,7 +69,8 @@
|
||||
"sharp": "^0.30.7",
|
||||
"systeminformation": "^5.12.1",
|
||||
"uuid": "^8.3.2",
|
||||
"yarn": "^1.22.19"
|
||||
"yarn": "^1.22.19",
|
||||
"zustand": "^4.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^12.1.4",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"pageTitle": {
|
||||
"label": "Page Title",
|
||||
"placeholder": "Homarr"
|
||||
},
|
||||
"metaTitle": {
|
||||
"label": "Meta Title",
|
||||
"placeholder": "Homarr 🦞"
|
||||
},
|
||||
"logo": {
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"form": {
|
||||
"configName": {
|
||||
"label": "Config name",
|
||||
"validation": {
|
||||
"required": "Config name is required"
|
||||
},
|
||||
"placeholder": "Your new config name"
|
||||
},
|
||||
"submitButton": "Confirm"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"title": "Module enabler"
|
||||
"title": "Enabled modules"
|
||||
}
|
||||
@@ -5,10 +5,14 @@
|
||||
"placeholderTip": "%s can be used as a placeholder for the query."
|
||||
},
|
||||
"customEngine": {
|
||||
"title": "Custom search engine",
|
||||
"label": "Query URL",
|
||||
"placeholder": "Custom query URL"
|
||||
},
|
||||
"searchNewTab": {
|
||||
"label": "Open search results in new tab"
|
||||
},
|
||||
"searchEnabled": {
|
||||
"label": "Search enabled"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,33 @@
|
||||
import { Center, Loader, Select, Tooltip } from '@mantine/core';
|
||||
import { setCookie } from 'cookies-next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { useState } from 'react';
|
||||
import { useConfigContext } from '../../config/provider';
|
||||
|
||||
export default function ConfigChanger() {
|
||||
const { config, loadConfig, setConfig, getConfigs } = useConfig();
|
||||
const [configList, setConfigList] = useState<string[]>([]);
|
||||
const [value, setValue] = useState(config.name);
|
||||
const { t } = useTranslation('settings/general/config-changer');
|
||||
const { name: configName } = useConfigContext();
|
||||
//const loadConfig = useConfigStore((x) => x.loadConfig);
|
||||
|
||||
const { data: configs, isLoading, isError } = useConfigsQuery();
|
||||
const [activeConfig, setActiveConfig] = useState(configName);
|
||||
|
||||
const onConfigChange = (value: string) => {
|
||||
// TODO: check what should happen here with @manuel-rw
|
||||
// Wheter it should check for the current url and then load the new config only on index
|
||||
// Or it should always load the selected config and open index or ?
|
||||
setActiveConfig(value);
|
||||
/*
|
||||
loadConfig(e ?? 'default');
|
||||
setCookie('config-name', e ?? 'default', {
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
sameSite: 'strict',
|
||||
});
|
||||
*/
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getConfigs().then((configs) => setConfigList(configs));
|
||||
}, [config]);
|
||||
// If configlist is empty, return a loading indicator
|
||||
if (configList.length === 0) {
|
||||
if (isLoading || !configs || configs?.length === 0 || !configName) {
|
||||
return (
|
||||
<Tooltip label={"Loading your configs. This doesn't load in vercel."}>
|
||||
<Center>
|
||||
@@ -23,23 +36,22 @@ export default function ConfigChanger() {
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
// return <Select data={[{ value: '1', label: '1' },]} onChange={(e) => console.log(e)} value="1" />;
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={t('configSelect.label')}
|
||||
value={value}
|
||||
defaultValue={config.name}
|
||||
onChange={(e) => {
|
||||
loadConfig(e ?? 'default');
|
||||
setCookie('config-name', e ?? 'default', {
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
sameSite: 'strict',
|
||||
});
|
||||
}}
|
||||
data={
|
||||
// If config list is empty, return the current config
|
||||
configList.length === 0 ? [config.name] : configList
|
||||
}
|
||||
value={activeConfig}
|
||||
onChange={onConfigChange}
|
||||
data={configs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const useConfigsQuery = () => {
|
||||
return useQuery({
|
||||
queryKey: ['config/get-all'],
|
||||
queryFn: fetchConfigs,
|
||||
});
|
||||
};
|
||||
|
||||
const fetchConfigs = async () => (await (await fetch('/api/configs')).json()) as string[];
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { Button, Group, Modal, TextInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import axios from 'axios';
|
||||
import fileDownload from 'js-file-download';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import {
|
||||
IconCheck as Check,
|
||||
IconDownload as Download,
|
||||
IconPlus as Plus,
|
||||
IconTrash as Trash,
|
||||
IconX as X,
|
||||
} from '@tabler/icons';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
export default function SaveConfigComponent(props: any) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const { config, setConfig } = useConfig();
|
||||
const { t } = useTranslation('settings/general/config-changer');
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
configName: config.name,
|
||||
},
|
||||
});
|
||||
function onClick(e: any) {
|
||||
if (config) {
|
||||
fileDownload(JSON.stringify(config, null, '\t'), `${config.name}.json`);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Group spacing="xs">
|
||||
<Modal radius="md" opened={opened} onClose={() => setOpened(false)} title={t('modal.title')}>
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
setConfig({ ...config, name: values.configName });
|
||||
setOpened(false);
|
||||
showNotification({
|
||||
title: t('modal.events.configSaved.title'),
|
||||
icon: <Check />,
|
||||
color: 'green',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: t('modal.events.configSaved.message', { configName: values.configName }),
|
||||
});
|
||||
})}
|
||||
>
|
||||
<TextInput
|
||||
required
|
||||
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>
|
||||
<Button size="xs" leftIcon={<Download />} variant="outline" onClick={onClick}>
|
||||
{t('buttons.download')}
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
leftIcon={<Trash />}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
axios
|
||||
.delete(`/api/configs/${config.name}`)
|
||||
.then(() => {
|
||||
showNotification({
|
||||
title: t('buttons.delete.notifications.deleted.title'),
|
||||
icon: <Check />,
|
||||
color: 'green',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: t('buttons.delete.notifications.deleted.message'),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
showNotification({
|
||||
title: t('buttons.delete.notifications.deleteFailed.title'),
|
||||
icon: <X />,
|
||||
color: 'red',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: t('buttons.delete.notifications.deleteFailed.message'),
|
||||
});
|
||||
});
|
||||
setConfig({ ...config, name: 'default' });
|
||||
}}
|
||||
>
|
||||
{t('buttons.delete.text')}
|
||||
</Button>
|
||||
<Button size="xs" leftIcon={<Plus />} variant="outline" onClick={() => setOpened(true)}>
|
||||
{t('buttons.saveCopy')}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
16
src/components/Dashboard/Dashboard.tsx
Normal file
16
src/components/Dashboard/Dashboard.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { DashboardDetailView } from './Views/DetailView';
|
||||
import { DashboardEditView } from './Views/EditView';
|
||||
import { useEditModeStore } from './Views/store';
|
||||
|
||||
interface DashboardProps {}
|
||||
|
||||
export const Dashboard = () => {
|
||||
const isEditMode = useEditModeStore((x) => x.enabled);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* The following elemens are splitted because gridstack doesn't reinitialize them when using same item. */}
|
||||
{isEditMode ? <DashboardEditView /> : <DashboardDetailView />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
91
src/components/Dashboard/Tiles/Service/Service.tsx
Normal file
91
src/components/Dashboard/Tiles/Service/Service.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { Card, Center, Text, UnstyledButton } from '@mantine/core';
|
||||
import { NextLink } from '@mantine/next';
|
||||
import { createStyles } from '@mantine/styles';
|
||||
import { ServiceType } from '../../../../types/service';
|
||||
import { useCardStyles } from '../../../layout/useCardStyles';
|
||||
import { useEditModeStore } from '../../Views/store';
|
||||
import { BaseTileProps } from '../type';
|
||||
|
||||
interface ServiceTileProps extends BaseTileProps {
|
||||
service: ServiceType;
|
||||
}
|
||||
|
||||
export const ServiceTile = ({ className, service }: ServiceTileProps) => {
|
||||
const isEditMode = useEditModeStore((x) => x.enabled);
|
||||
|
||||
const { cx, classes } = useStyles();
|
||||
|
||||
const {
|
||||
classes: { card: cardClass },
|
||||
} = useCardStyles();
|
||||
|
||||
const inner = (
|
||||
<>
|
||||
<Text align="center" weight={500} size="md" className={classes.serviceName}>
|
||||
{service.name}
|
||||
</Text>
|
||||
<Center style={{ height: '75%', flex: 1 }}>
|
||||
<img className={classes.image} src={service.appearance.iconUrl} alt="" />
|
||||
</Center>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className={cx(className, cardClass)} withBorder radius="lg" shadow="md">
|
||||
{isEditMode &&
|
||||
{
|
||||
/*<AppShelfMenu service={service} />*/
|
||||
}}{' '}
|
||||
{/* TODO: change to serviceMenu */}
|
||||
{!service.url || isEditMode ? (
|
||||
<UnstyledButton
|
||||
className={classes.button}
|
||||
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
|
||||
>
|
||||
{inner}
|
||||
</UnstyledButton>
|
||||
) : (
|
||||
<UnstyledButton
|
||||
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
|
||||
component={NextLink}
|
||||
href={service.url}
|
||||
target={service.behaviour.isOpeningNewTab ? '_blank' : '_self'}
|
||||
className={cx(classes.button, classes.link)}
|
||||
>
|
||||
{inner}
|
||||
</UnstyledButton>
|
||||
)}
|
||||
{/*<ServicePing service={service} />*/}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = createStyles((theme, _params, getRef) => {
|
||||
return {
|
||||
image: {
|
||||
ref: getRef('image'),
|
||||
maxHeight: '80%',
|
||||
maxWidth: '80%',
|
||||
transition: 'transform 100ms ease-in-out',
|
||||
},
|
||||
serviceName: {
|
||||
ref: getRef('serviceName'),
|
||||
},
|
||||
button: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
link: {
|
||||
[`&:hover .${getRef('image')}`]: {
|
||||
// TODO: add styles for image when hovering card
|
||||
},
|
||||
[`&:hover .${getRef('serviceName')}`]: {
|
||||
// TODO: add styles for service name when hovering card
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
48
src/components/Dashboard/Tiles/TileWrapper.tsx
Normal file
48
src/components/Dashboard/Tiles/TileWrapper.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ReactNode, RefObject } from 'react';
|
||||
|
||||
interface GridstackTileWrapperProps {
|
||||
id: string;
|
||||
type: 'service' | 'module';
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
minWidth?: number;
|
||||
minHeight?: number;
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
itemRef: RefObject<HTMLDivElement>;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const GridstackTileWrapper = ({
|
||||
id,
|
||||
type,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
minWidth,
|
||||
minHeight,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
children,
|
||||
itemRef,
|
||||
}: GridstackTileWrapperProps) => (
|
||||
<div
|
||||
className="grid-stack-item"
|
||||
data-type={type}
|
||||
data-id={id}
|
||||
gs-x={x}
|
||||
gs-y={y}
|
||||
gs-w={width}
|
||||
gs-h={height}
|
||||
gs-min-w={minWidth}
|
||||
gs-min-h={minHeight}
|
||||
gs-max-w={maxWidth}
|
||||
gs-max-h={maxHeight}
|
||||
ref={itemRef}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
76
src/components/Dashboard/Tiles/definition.tsx
Normal file
76
src/components/Dashboard/Tiles/definition.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { IntegrationsType } from '../../../types/integration';
|
||||
import { ServiceTile } from './Service/Service';
|
||||
/*import { CalendarTile } from './calendar';
|
||||
import { ClockTile } from './clock';
|
||||
import { DashDotTile } from './dash-dot';
|
||||
import { WeatherTile } from './weather';*/
|
||||
|
||||
type TileDefinitionProps = {
|
||||
[key in keyof IntegrationsType | 'service']: {
|
||||
minWidth?: number;
|
||||
minHeight?: number;
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
component: React.ElementType;
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: change components for other modules
|
||||
export const Tiles: TileDefinitionProps = {
|
||||
service: {
|
||||
component: ServiceTile,
|
||||
minWidth: 2,
|
||||
maxWidth: 12,
|
||||
minHeight: 2,
|
||||
maxHeight: 12,
|
||||
},
|
||||
bitTorrent: {
|
||||
component: CalendarTile,
|
||||
minWidth: 4,
|
||||
maxWidth: 12,
|
||||
minHeight: 5,
|
||||
maxHeight: 12,
|
||||
},
|
||||
calendar: {
|
||||
component: CalendarTile,
|
||||
minWidth: 4,
|
||||
maxWidth: 12,
|
||||
minHeight: 5,
|
||||
maxHeight: 12,
|
||||
},
|
||||
clock: {
|
||||
component: ClockTile,
|
||||
minWidth: 4,
|
||||
maxWidth: 12,
|
||||
minHeight: 2,
|
||||
maxHeight: 12,
|
||||
},
|
||||
dashDot: {
|
||||
component: DashDotTile,
|
||||
minWidth: 4,
|
||||
maxWidth: 9,
|
||||
minHeight: 5,
|
||||
maxHeight: 14,
|
||||
},
|
||||
torrentNetworkTraffic: {
|
||||
component: CalendarTile,
|
||||
minWidth: 4,
|
||||
maxWidth: 12,
|
||||
minHeight: 5,
|
||||
maxHeight: 12,
|
||||
},
|
||||
useNet: {
|
||||
component: CalendarTile,
|
||||
minWidth: 4,
|
||||
maxWidth: 12,
|
||||
minHeight: 5,
|
||||
maxHeight: 12,
|
||||
},
|
||||
weather: {
|
||||
component: WeatherTile,
|
||||
minWidth: 4,
|
||||
maxWidth: 12,
|
||||
minHeight: 2,
|
||||
maxHeight: 12,
|
||||
},
|
||||
};
|
||||
3
src/components/Dashboard/Tiles/type.ts
Normal file
3
src/components/Dashboard/Tiles/type.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface BaseTileProps {
|
||||
className?: string;
|
||||
}
|
||||
5
src/components/Dashboard/Views/DetailView.tsx
Normal file
5
src/components/Dashboard/Views/DetailView.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { DashboardView } from './main';
|
||||
|
||||
export const DashboardDetailView = () => {
|
||||
return <DashboardView />;
|
||||
};
|
||||
5
src/components/Dashboard/Views/EditView.tsx
Normal file
5
src/components/Dashboard/Views/EditView.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { DashboardView } from './main';
|
||||
|
||||
export const DashboardEditView = () => {
|
||||
return <DashboardView />;
|
||||
};
|
||||
39
src/components/Dashboard/Views/main.tsx
Normal file
39
src/components/Dashboard/Views/main.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Group, Stack } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { ServiceTile } from '../Tiles/Service/Service';
|
||||
import { DashboardSidebar } from '../Wrappers/Sidebar/Sidebar';
|
||||
|
||||
export const DashboardView = () => {
|
||||
const wrappers = useWrapperItems();
|
||||
|
||||
return (
|
||||
<Group align="top" h="100%">
|
||||
{/*<DashboardSidebar location="left" />*/}
|
||||
<Stack mx={-10} style={{ flexGrow: 1 }}>
|
||||
{wrappers.map(
|
||||
(item) =>
|
||||
item.type === 'category'
|
||||
? 'category' //<DashboardCategory key={item.id} category={item as unknown as CategoryType} />
|
||||
: 'wrapper' //<DashboardWrapper key={item.id} wrapper={item as WrapperType} />
|
||||
)}
|
||||
</Stack>
|
||||
{/*<DashboardSidebar location="right" />*/}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
const useWrapperItems = () => {
|
||||
const { config } = useConfigContext();
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
config
|
||||
? [
|
||||
...config.categories.map((c) => ({ ...c, type: 'category' })),
|
||||
...config.wrappers.map((w) => ({ ...w, type: 'wrapper' })),
|
||||
].sort((a, b) => a.position - b.position)
|
||||
: [],
|
||||
[config?.categories, config?.wrappers]
|
||||
);
|
||||
};
|
||||
11
src/components/Dashboard/Views/store.ts
Normal file
11
src/components/Dashboard/Views/store.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import create from 'zustand';
|
||||
|
||||
interface EditModeState {
|
||||
enabled: boolean;
|
||||
toggleEditMode: () => void;
|
||||
}
|
||||
|
||||
export const useEditModeStore = create<EditModeState>((set) => ({
|
||||
enabled: false,
|
||||
toggleEditMode: () => set((state) => ({ enabled: !state.enabled })),
|
||||
}));
|
||||
72
src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx
Normal file
72
src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Card } from '@mantine/core';
|
||||
import { RefObject } from 'react';
|
||||
import { Tiles } from '../../Tiles/definition';
|
||||
import { GridstackTileWrapper } from '../../Tiles/TileWrapper';
|
||||
import { useGridstack } from '../gridstack/use-gridstack';
|
||||
|
||||
interface DashboardSidebarProps {
|
||||
location: 'right' | 'left';
|
||||
}
|
||||
|
||||
export const DashboardSidebar = ({ location }: DashboardSidebarProps) => {
|
||||
const { refs, items, integrations } = useGridstack('sidebar', location);
|
||||
|
||||
const minRow = useMinRowForFullHeight(refs.wrapper);
|
||||
|
||||
return (
|
||||
<Card
|
||||
withBorder
|
||||
w={300}
|
||||
style={{
|
||||
background: 'none',
|
||||
borderStyle: 'dashed',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="grid-stack grid-stack-sidebar"
|
||||
style={{ transitionDuration: '0s', height: '100%' }}
|
||||
data-sidebar={location}
|
||||
gs-min-row={minRow}
|
||||
ref={refs.wrapper}
|
||||
>
|
||||
{items.map((service) => {
|
||||
const { component: TileComponent, ...tile } = Tiles['service'];
|
||||
return (
|
||||
<GridstackTileWrapper
|
||||
id={service.id}
|
||||
type="service"
|
||||
key={service.id}
|
||||
itemRef={refs.items.current[service.id]}
|
||||
{...tile}
|
||||
{...service.shape.location}
|
||||
{...service.shape.size}
|
||||
>
|
||||
<TileComponent className="grid-stack-item-content" service={service} />
|
||||
</GridstackTileWrapper>
|
||||
);
|
||||
})}
|
||||
{Object.entries(integrations).map(([k, v]) => {
|
||||
const { component: TileComponent, ...tile } = Tiles[k as keyof typeof Tiles];
|
||||
|
||||
return (
|
||||
<GridstackTileWrapper
|
||||
id={k}
|
||||
type="module"
|
||||
key={k}
|
||||
itemRef={refs.items.current[k]}
|
||||
{...tile}
|
||||
{...v.shape.location}
|
||||
{...v.shape.size}
|
||||
>
|
||||
<TileComponent className="grid-stack-item-content" module={v} />
|
||||
</GridstackTileWrapper>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const useMinRowForFullHeight = (wrapperRef: RefObject<HTMLDivElement>) => {
|
||||
return wrapperRef.current ? Math.floor(wrapperRef.current!.offsetHeight / 64) : 2;
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { GridStack, GridStackNode } from 'fily-publish-gridstack';
|
||||
import { MutableRefObject, RefObject } from 'react';
|
||||
import { IntegrationsType } from '../../../../types/integration';
|
||||
import { ServiceType } from '../../../../types/service';
|
||||
|
||||
export const initializeGridstack = (
|
||||
areaType: 'wrapper' | 'category' | 'sidebar',
|
||||
wrapperRef: RefObject<HTMLDivElement>,
|
||||
gridRef: MutableRefObject<GridStack | undefined>,
|
||||
itemRefs: MutableRefObject<Record<string, RefObject<HTMLDivElement>>>,
|
||||
areaId: string,
|
||||
items: ServiceType[],
|
||||
integrations: IntegrationsType,
|
||||
isEditMode: boolean,
|
||||
events: {
|
||||
onChange: (changedNode: GridStackNode) => void;
|
||||
onAdd: (addedNode: GridStackNode) => void;
|
||||
}
|
||||
) => {
|
||||
if (!wrapperRef.current) return;
|
||||
// calculates the currently available count of columns
|
||||
const columnCount = areaType === 'sidebar' ? 4 : Math.floor(wrapperRef.current.offsetWidth / 64);
|
||||
const minRow = areaType !== 'sidebar' ? 1 : Math.floor(wrapperRef.current.offsetHeight / 64);
|
||||
// initialize gridstack
|
||||
gridRef.current = GridStack.init(
|
||||
{
|
||||
column: columnCount,
|
||||
margin: 10,
|
||||
cellHeight: 64,
|
||||
float: true,
|
||||
alwaysShowResizeHandle: 'mobile',
|
||||
acceptWidgets: true,
|
||||
disableOneColumnMode: true,
|
||||
staticGrid: !isEditMode,
|
||||
minRow,
|
||||
},
|
||||
// selector of the gridstack item (it's eather category or wrapper)
|
||||
`.grid-stack-${areaType}[data-${areaType}='${areaId}']`
|
||||
);
|
||||
const grid = gridRef.current;
|
||||
|
||||
// Add listener for moving items around in a wrapper
|
||||
grid.on('change', (_, el) => {
|
||||
const nodes = el as GridStackNode[];
|
||||
const firstNode = nodes.at(0);
|
||||
if (!firstNode) return;
|
||||
events.onChange(firstNode);
|
||||
});
|
||||
|
||||
// Add listener for moving items in config from one wrapper to another
|
||||
grid.on('added', (_, el) => {
|
||||
const nodes = el as GridStackNode[];
|
||||
const firstNode = nodes.at(0);
|
||||
if (!firstNode) return;
|
||||
events.onAdd(firstNode);
|
||||
});
|
||||
grid.batchUpdate();
|
||||
grid.removeAll(false);
|
||||
items.forEach(
|
||||
({ id }) =>
|
||||
itemRefs.current[id] && grid.makeWidget(itemRefs.current[id].current as HTMLDivElement)
|
||||
);
|
||||
Object.keys(integrations).forEach(
|
||||
(key) =>
|
||||
itemRefs.current[key] && grid.makeWidget(itemRefs.current[key].current as HTMLDivElement)
|
||||
);
|
||||
grid.batchUpdate(false);
|
||||
};
|
||||
231
src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts
Normal file
231
src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { GridStack, GridStackNode } from 'fily-publish-gridstack';
|
||||
import {
|
||||
createRef,
|
||||
LegacyRef,
|
||||
MutableRefObject,
|
||||
RefObject,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
import { useResize } from '../../../../hooks/use-resize';
|
||||
import { IntegrationsType } from '../../../../types/integration';
|
||||
import { ServiceType } from '../../../../types/service';
|
||||
import { TileBaseType } from '../../../../types/tile';
|
||||
import { useEditModeStore } from '../../Views/store';
|
||||
import { initializeGridstack } from './init-gridstack';
|
||||
|
||||
interface UseGristackReturnType {
|
||||
items: ServiceType[];
|
||||
integrations: Partial<IntegrationsType>;
|
||||
refs: {
|
||||
wrapper: RefObject<HTMLDivElement>;
|
||||
items: MutableRefObject<Record<string, RefObject<HTMLDivElement>>>;
|
||||
gridstack: MutableRefObject<GridStack | undefined>;
|
||||
};
|
||||
}
|
||||
|
||||
export const useGridstack = (
|
||||
areaType: 'wrapper' | 'category' | 'sidebar',
|
||||
areaId: string
|
||||
): UseGristackReturnType => {
|
||||
const isEditMode = useEditModeStore((x) => x.enabled);
|
||||
const { config, name: configName } = useConfigContext();
|
||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
||||
// define reference for wrapper - is used to calculate the width of the wrapper
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
// references to the diffrent items contained in the gridstack
|
||||
const itemRefs = useRef<Record<string, RefObject<HTMLDivElement>>>({});
|
||||
// reference of the gridstack object for modifications after initialization
|
||||
const gridRef = useRef<GridStack>();
|
||||
// width of the wrapper (updating on page resize)
|
||||
const { width, height } = useResize(wrapperRef);
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
config?.services.filter(
|
||||
(x) =>
|
||||
x.area.type === areaType &&
|
||||
(x.area.type === 'sidebar'
|
||||
? x.area.properties.location === areaId
|
||||
: x.area.properties.id === areaId)
|
||||
) ?? [],
|
||||
[config]
|
||||
);
|
||||
const integrations = useMemo(() => {
|
||||
if (!config) return;
|
||||
return (Object.entries(config.integrations) as [keyof IntegrationsType, TileBaseType][])
|
||||
.filter(
|
||||
([k, v]) =>
|
||||
v.area.type === areaType &&
|
||||
(v.area.type === 'sidebar'
|
||||
? v.area.properties.location === areaId
|
||||
: v.area.properties.id === areaId)
|
||||
)
|
||||
.reduce((prev, [k, v]) => {
|
||||
prev[k] = v as unknown as any;
|
||||
return prev;
|
||||
}, {} as IntegrationsType);
|
||||
}, [config]);
|
||||
|
||||
// define items in itemRefs for easy access and reference to items
|
||||
if (
|
||||
Object.keys(itemRefs.current).length !==
|
||||
items.length + Object.keys(integrations ?? {}).length
|
||||
) {
|
||||
items.forEach(({ id }: { id: keyof typeof itemRefs.current }) => {
|
||||
itemRefs.current[id] = itemRefs.current[id] || createRef();
|
||||
});
|
||||
Object.keys(integrations ?? {}).forEach((k) => {
|
||||
itemRefs.current[k] = itemRefs.current[k] || createRef();
|
||||
});
|
||||
}
|
||||
|
||||
// change column count depending on the width and the gridRef
|
||||
useEffect(() => {
|
||||
if (areaType === 'sidebar') return;
|
||||
gridRef.current?.column(Math.floor(width / 64), 'moveScale');
|
||||
}, [gridRef, width]);
|
||||
|
||||
const onChange = isEditMode
|
||||
? (changedNode: GridStackNode) => {
|
||||
if (!configName) return;
|
||||
|
||||
const itemType = changedNode.el?.getAttribute('data-type');
|
||||
const itemId = changedNode.el?.getAttribute('data-id');
|
||||
if (!itemType || !itemId) return;
|
||||
|
||||
// Updates the config and defines the new position of the item
|
||||
updateConfig(configName, (previous) => {
|
||||
const currentItem =
|
||||
itemType === 'service'
|
||||
? previous.services.find((x) => x.id === itemId)
|
||||
: previous.integrations[itemId as keyof typeof previous.integrations];
|
||||
if (!currentItem) return previous;
|
||||
|
||||
currentItem.shape = {
|
||||
location: {
|
||||
x: changedNode.x ?? currentItem.shape.location.x,
|
||||
y: changedNode.y ?? currentItem.shape.location.y,
|
||||
},
|
||||
size: {
|
||||
width: changedNode.w ?? currentItem.shape.size.width,
|
||||
height: changedNode.h ?? currentItem.shape.size.height,
|
||||
},
|
||||
};
|
||||
|
||||
if (itemType === 'service') {
|
||||
return {
|
||||
...previous,
|
||||
services: [
|
||||
...previous.services.filter((x) => x.id !== itemId),
|
||||
{ ...(currentItem as ServiceType) },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const integrationsCopy = { ...previous.integrations };
|
||||
integrationsCopy[itemId as keyof typeof integrationsCopy] = currentItem as any;
|
||||
return {
|
||||
...previous,
|
||||
integrations: integrationsCopy,
|
||||
};
|
||||
});
|
||||
}
|
||||
: () => {};
|
||||
|
||||
const onAdd = isEditMode
|
||||
? (addedNode: GridStackNode) => {
|
||||
if (!configName) return;
|
||||
|
||||
const itemType = addedNode.el?.getAttribute('data-type');
|
||||
const itemId = addedNode.el?.getAttribute('data-id');
|
||||
if (!itemType || !itemId) return;
|
||||
|
||||
// Updates the config and defines the new position and wrapper of the item
|
||||
updateConfig(configName, (previous) => {
|
||||
const currentItem =
|
||||
itemType === 'service'
|
||||
? previous.services.find((x) => x.id === itemId)
|
||||
: previous.integrations[itemId as keyof typeof previous.integrations];
|
||||
|
||||
if (!currentItem) return previous;
|
||||
|
||||
if (areaType === 'sidebar') {
|
||||
currentItem.area = {
|
||||
type: areaType,
|
||||
properties: {
|
||||
location: areaId as 'right' | 'left',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
currentItem.area = {
|
||||
type: areaType,
|
||||
properties: {
|
||||
id: areaId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
currentItem.shape = {
|
||||
location: {
|
||||
x: addedNode.x ?? currentItem.shape.location.x,
|
||||
y: addedNode.y ?? currentItem.shape.location.y,
|
||||
},
|
||||
size: {
|
||||
width: addedNode.w ?? currentItem.shape.size.width,
|
||||
height: addedNode.h ?? currentItem.shape.size.height,
|
||||
},
|
||||
};
|
||||
|
||||
if (itemType === 'service') {
|
||||
return {
|
||||
...previous,
|
||||
services: [
|
||||
...previous.services.filter((x) => x.id !== itemId),
|
||||
{ ...(currentItem as ServiceType) },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const integrationsCopy = { ...previous.integrations };
|
||||
integrationsCopy[itemId as keyof typeof integrationsCopy] = currentItem as any;
|
||||
return {
|
||||
...previous,
|
||||
integrations: integrationsCopy,
|
||||
};
|
||||
});
|
||||
}
|
||||
: () => {};
|
||||
|
||||
// initialize the gridstack
|
||||
useLayoutEffect(() => {
|
||||
initializeGridstack(
|
||||
areaType,
|
||||
wrapperRef,
|
||||
gridRef,
|
||||
itemRefs,
|
||||
areaId,
|
||||
items,
|
||||
integrations ?? {},
|
||||
isEditMode,
|
||||
{
|
||||
onChange,
|
||||
onAdd,
|
||||
}
|
||||
);
|
||||
}, [items.length, wrapperRef.current, Object.keys(integrations ?? {}).length]);
|
||||
|
||||
return {
|
||||
items,
|
||||
integrations: integrations ?? {},
|
||||
refs: {
|
||||
items: itemRefs,
|
||||
wrapper: wrapperRef,
|
||||
gridstack: gridRef,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createStyles, Switch, Group } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
position: 'relative',
|
||||
'& *': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
|
||||
icon: {
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
top: 3,
|
||||
},
|
||||
|
||||
iconLight: {
|
||||
left: 4,
|
||||
color: theme.white,
|
||||
},
|
||||
|
||||
iconDark: {
|
||||
right: 4,
|
||||
color: theme.colors.gray[6],
|
||||
},
|
||||
}));
|
||||
|
||||
export function SearchNewTabSwitch() {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { classes, cx } = useStyles();
|
||||
const defaultPosition = config?.settings?.searchNewTab ?? true;
|
||||
const [openInNewTab, setOpenInNewTab] = useState<boolean>(defaultPosition);
|
||||
const { t } = useTranslation('settings/general/search-engine');
|
||||
const toggleOpenInNewTab = () => {
|
||||
setOpenInNewTab(!openInNewTab);
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
searchNewTab: !openInNewTab,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<div className={classes.root}>
|
||||
<Switch checked={openInNewTab} onChange={() => toggleOpenInNewTab()} size="md" />
|
||||
</div>
|
||||
{t('searchNewTab.label')}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
<SearchEngineSelector searchEngine={config.settings.common.searchEngine} />
|
||||
<SearchNewTabSwitch
|
||||
defaultValue={config.settings.common.searchEngine.properties.openInNewTab}
|
||||
/>
|
||||
{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 />
|
||||
<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>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createStyles, Switch, Group } from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
position: 'relative',
|
||||
'& *': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
|
||||
icon: {
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
top: 3,
|
||||
},
|
||||
|
||||
iconLight: {
|
||||
left: 4,
|
||||
color: theme.white,
|
||||
},
|
||||
|
||||
iconDark: {
|
||||
right: 4,
|
||||
color: theme.colors.gray[6],
|
||||
},
|
||||
}));
|
||||
|
||||
export function WidgetsPositionSwitch() {
|
||||
const { config, setConfig } = useConfig();
|
||||
const { classes, cx } = useStyles();
|
||||
const defaultPosition = config?.settings?.widgetPosition || 'right';
|
||||
const [widgetPosition, setWidgetPosition] = useState(defaultPosition);
|
||||
const { t } = useTranslation('settings/general/widget-positions');
|
||||
const toggleWidgetPosition = () => {
|
||||
const position = widgetPosition === 'right' ? 'left' : 'right';
|
||||
setWidgetPosition(position);
|
||||
setConfig({
|
||||
...config,
|
||||
settings: {
|
||||
...config.settings,
|
||||
widgetPosition: position,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
label={t('label')}
|
||||
checked={widgetPosition === 'left'}
|
||||
onChange={() => toggleWidgetPosition()}
|
||||
size="md"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Global } from '@mantine/core';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { useConfigContext } from '../../config/provider';
|
||||
|
||||
export function Background() {
|
||||
const { config } = useConfig();
|
||||
const { config } = useConfigContext();
|
||||
|
||||
return (
|
||||
<Global
|
||||
styles={{
|
||||
body: {
|
||||
minHeight: '100vh',
|
||||
backgroundImage: `url('${config.settings.background}')` || '',
|
||||
backgroundImage: `url('${config?.settings.customization.backgroundImageUrl}')` || '',
|
||||
backgroundPosition: 'center center',
|
||||
backgroundSize: 'cover',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
|
||||
34
src/components/layout/Head/Head.tsx
Normal file
34
src/components/layout/Head/Head.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable react/no-invalid-html-attribute */
|
||||
import React from 'react';
|
||||
import NextHead from 'next/head';
|
||||
import { SafariStatusBarStyle } from './SafariStatusBarStyle';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
|
||||
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-squared.png'}
|
||||
/>
|
||||
<meta
|
||||
name="apple-mobile-web-app-title"
|
||||
content={config?.settings.customization.metaTitle || 'Homarr'}
|
||||
/>
|
||||
|
||||
<SafariStatusBarStyle />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
</NextHead>
|
||||
);
|
||||
}
|
||||
12
src/components/layout/Head/SafariStatusBarStyle.tsx
Normal file
12
src/components/layout/Head/SafariStatusBarStyle.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useMantineTheme } from '@mantine/core';
|
||||
|
||||
export const SafariStatusBarStyle = () => {
|
||||
const { colorScheme } = useMantineTheme();
|
||||
const isDark = colorScheme === 'dark';
|
||||
return (
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content={isDark ? 'white-translucent' : 'black-translucent'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,48 +1,31 @@
|
||||
import { AppShell, createStyles } from '@mantine/core';
|
||||
import { Header } from './header/Header';
|
||||
import { Footer } from './Footer';
|
||||
import Aside from './Aside';
|
||||
import Navbar from './Navbar';
|
||||
import { HeaderConfig } from './header/HeaderConfig';
|
||||
import { useConfigContext } from '../../config/provider';
|
||||
import { Background } from './Background';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { Footer } from './Footer';
|
||||
import { Header } from './Header/Header';
|
||||
import { Head } from './Head/Head';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
main: {},
|
||||
appShell: {
|
||||
// eslint-disable-next-line no-useless-computed-key
|
||||
['@media screen and (display-mode: standalone)']: {
|
||||
'&': {
|
||||
paddingTop: '88px !important',
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
const useStyles = createStyles(() => ({}));
|
||||
|
||||
export default function Layout({ children, style }: any) {
|
||||
const { classes, cx } = useStyles();
|
||||
const { config } = useConfig();
|
||||
const widgetPosition = config?.settings?.widgetPosition === 'left';
|
||||
export default function Layout({ children }: any) {
|
||||
const { cx } = useStyles();
|
||||
const { config } = useConfigContext();
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
fixed={false}
|
||||
header={<Header />}
|
||||
navbar={widgetPosition ? <Navbar /> : undefined}
|
||||
aside={widgetPosition ? undefined : <Aside />}
|
||||
footer={<Footer links={[]} />}
|
||||
>
|
||||
<HeaderConfig />
|
||||
<Background />
|
||||
<main
|
||||
className={cx(classes.main)}
|
||||
style={{
|
||||
...style,
|
||||
styles={{
|
||||
main: {
|
||||
minHeight: 'calc(100vh - var(--mantine-header-height))',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Head />
|
||||
<Background />
|
||||
{children}
|
||||
</main>
|
||||
<style>{cx(config.settings.customCSS)}</style>
|
||||
<style>{cx(config?.settings.customization.customCss)}</style>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
import { Group, Image, Text } from '@mantine/core';
|
||||
import { NextLink } from '@mantine/next';
|
||||
import * as React from 'react';
|
||||
import { useConfigContext } from '../../config/provider';
|
||||
import { useColorTheme } from '../../tools/color';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
export function Logo({ style, withoutText }: any) {
|
||||
const { config } = useConfig();
|
||||
interface LogoProps {
|
||||
size?: 'md' | 'xs';
|
||||
withoutText?: boolean;
|
||||
}
|
||||
|
||||
export function Logo({ size = 'md', withoutText = false }: LogoProps) {
|
||||
const { config } = useConfigContext();
|
||||
const { primaryColor, secondaryColor } = useColorTheme();
|
||||
|
||||
return (
|
||||
<Group spacing="xs" noWrap>
|
||||
<Group spacing={size === 'md' ? 'xs' : 4} noWrap>
|
||||
<Image
|
||||
width={50}
|
||||
src={config.settings.logo || '/imgs/logo/logo.png'}
|
||||
width={size === 'md' ? 50 : 20}
|
||||
src={config?.settings.customization.logoImageUrl || '/imgs/logo/logo.png'}
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
/>
|
||||
{withoutText ? null : (
|
||||
<Text
|
||||
sx={style}
|
||||
size={size === 'md' ? 22 : 'xs'}
|
||||
weight="bold"
|
||||
variant="gradient"
|
||||
gradient={{
|
||||
@@ -28,7 +31,7 @@ export function Logo({ style, withoutText }: any) {
|
||||
deg: 145,
|
||||
}}
|
||||
>
|
||||
{config.settings.title || 'Homarr'}
|
||||
{config?.settings.customization.pageTitle || 'Homarr'}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
@@ -1,38 +1,40 @@
|
||||
import { Group, Header as Head, useMantineColorScheme, useMantineTheme } from '@mantine/core';
|
||||
import { Box, createStyles, Group, Header as MantineHeader, useMantineTheme } from '@mantine/core';
|
||||
import { useViewportSize } from '@mantine/hooks';
|
||||
import { AddItemShelfButton } from '../../AppShelf/AddAppShelfItem';
|
||||
|
||||
import DockerMenuButton from '../../../modules/docker/DockerModule';
|
||||
import { Search } from './Search';
|
||||
import { SettingsMenuButton } from '../../Settings/SettingsMenu';
|
||||
import { Logo } from '../Logo';
|
||||
import { useConfig } from '../../../tools/state';
|
||||
import { SearchModuleComponent } from '../../../modules/search/SearchModule';
|
||||
import { useCardStyles } from '../useCardStyles';
|
||||
|
||||
export const HeaderHeight = 64;
|
||||
|
||||
export function Header(props: any) {
|
||||
const { width } = useViewportSize();
|
||||
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
|
||||
const { config } = useConfig();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const { classes } = useStyles();
|
||||
const { classes: cardClasses } = useCardStyles();
|
||||
|
||||
return (
|
||||
<Head
|
||||
height="auto"
|
||||
style={{
|
||||
background: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '255, 255, 255,'} \
|
||||
${(config.settings.appOpacity || 100) / 100}`,
|
||||
borderColor: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '233, 236, 239,'} \
|
||||
${(config.settings.appOpacity || 100) / 100}`,
|
||||
}}
|
||||
>
|
||||
<MantineHeader height={HeaderHeight} className={cardClasses.card}>
|
||||
<Group p="xs" noWrap grow>
|
||||
{width > MIN_WIDTH_MOBILE && <Logo style={{ fontSize: 22 }} />}
|
||||
<Box className={classes.hide}>
|
||||
<Logo />
|
||||
</Box>
|
||||
<Group position="right" noWrap>
|
||||
<SearchModuleComponent />
|
||||
<Search />
|
||||
<DockerMenuButton />
|
||||
<SettingsMenuButton />
|
||||
<AddItemShelfButton />
|
||||
</Group>
|
||||
</Group>
|
||||
</Head>
|
||||
</MantineHeader>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
hide: {
|
||||
[theme.fn.smallerThan('xs')]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/* eslint-disable react/no-invalid-html-attribute */
|
||||
import React from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useConfig } from '../../../tools/state';
|
||||
import { SafariStatusBarStyle } from './safariStatusBarStyle';
|
||||
|
||||
export function HeaderConfig(props: any) {
|
||||
const { config } = useConfig();
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<title>{config.settings.title || 'Homarr 🦞'}</title>
|
||||
<link rel="shortcut icon" href={config.settings.favicon || '/imgs/favicon/favicon.svg'} />
|
||||
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
{/* configure apple splash screen & touch icon */}
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
href={config.settings.favicon || '/imgs/favicon/favicon-squared.png'}
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content={config.settings.title || 'Homarr'} />
|
||||
|
||||
<SafariStatusBarStyle />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
@@ -8,26 +8,25 @@ import {
|
||||
Menu,
|
||||
Popover,
|
||||
ScrollArea,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { IconSearch, IconBrandYoutube, IconDownload, IconMovie } from '@tabler/icons';
|
||||
import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
||||
import { useDebouncedValue, useHotkeys } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { IconBrandYoutube, IconDownload, IconMovie, IconSearch } from '@tabler/icons';
|
||||
import axios from 'axios';
|
||||
import { IModule } from '../ModuleTypes';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { OverseerrModule } from '../overseerr';
|
||||
import Tip from '../../components/layout/Tip';
|
||||
import { OverseerrMediaDisplay } from '../common';
|
||||
import SmallServiceItem from '../../components/AppShelf/SmallServiceItem';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
||||
import SmallServiceItem from '../../AppShelf/SmallServiceItem';
|
||||
import Tip from '../Tip';
|
||||
import { searchUrls } from '../../Settings/Common/SearchEngineSelector';
|
||||
import { useConfigContext } from '../../../config/provider';
|
||||
import { OverseerrMediaDisplay } from '../../../modules/common';
|
||||
import { IModule } from '../../../modules/ModuleTypes';
|
||||
|
||||
export const SearchModule: IModule = {
|
||||
title: 'Search',
|
||||
icon: IconSearch,
|
||||
component: SearchModuleComponent,
|
||||
component: Search,
|
||||
id: 'search',
|
||||
};
|
||||
|
||||
@@ -50,15 +49,24 @@ const useStyles = createStyles((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export function SearchModuleComponent() {
|
||||
const { config } = useConfig();
|
||||
export function Search() {
|
||||
const { t } = useTranslation('modules/search');
|
||||
const { config } = useConfigContext();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [debounced, cancel] = useDebouncedValue(searchQuery, 250);
|
||||
const isOverseerrEnabled = config.modules?.[OverseerrModule.id]?.enabled ?? false;
|
||||
const OverseerrService = config.services.find(
|
||||
(service) => service.type === 'Overseerr' || service.type === 'Jellyseerr'
|
||||
|
||||
// TODO: ask manuel-rw about overseerr
|
||||
const isOverseerrEnabled = false; //config?.settings.common.enabledModules.overseerr;
|
||||
const overseerrService = config?.services.find(
|
||||
(service) =>
|
||||
service.integration?.type === 'overseerr' || service.integration?.type === 'jellyseerr'
|
||||
);
|
||||
const searchEngineSettings = config?.settings.common.searchEngine;
|
||||
const searchEngineUrl = !searchEngineSettings
|
||||
? searchUrls.google
|
||||
: searchEngineSettings.type === 'custom'
|
||||
? searchEngineSettings.properties.template
|
||||
: searchUrls[searchEngineSettings.type];
|
||||
|
||||
const searchEnginesList: ItemProps[] = [
|
||||
{
|
||||
@@ -67,7 +75,7 @@ export function SearchModuleComponent() {
|
||||
label: t('searchEngines.search.name'),
|
||||
value: 'search',
|
||||
description: t('searchEngines.search.description'),
|
||||
url: config.settings.searchUrl,
|
||||
url: searchEngineUrl,
|
||||
shortcut: 's',
|
||||
},
|
||||
{
|
||||
@@ -90,26 +98,27 @@ export function SearchModuleComponent() {
|
||||
},
|
||||
{
|
||||
icon: <IconMovie />,
|
||||
disabled: !(isOverseerrEnabled === true && OverseerrService !== undefined),
|
||||
disabled: !(isOverseerrEnabled === true && overseerrService !== undefined),
|
||||
label: t('searchEngines.overseerr.name'),
|
||||
value: 'overseerr',
|
||||
description: t('searchEngines.overseerr.description'),
|
||||
url: `${OverseerrService?.url}search?query=`,
|
||||
url: `${overseerrService?.url}search?query=`,
|
||||
shortcut: 'm',
|
||||
},
|
||||
];
|
||||
const [selectedSearchEngine, setSearchEngine] = useState<ItemProps>(searchEnginesList[0]);
|
||||
const matchingServices = config.services.filter((service) => {
|
||||
const matchingServices =
|
||||
config?.services.filter((service) => {
|
||||
if (searchQuery === '' || searchQuery === undefined) {
|
||||
return false;
|
||||
}
|
||||
return service.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
});
|
||||
}) ?? [];
|
||||
const autocompleteData = matchingServices.map((service) => ({
|
||||
label: service.name,
|
||||
value: service.name,
|
||||
icon: service.icon,
|
||||
url: service.openedUrl ?? service.url,
|
||||
icon: service.appearance.iconUrl,
|
||||
url: service.behaviour.onClickUrl ?? service.url,
|
||||
}));
|
||||
const AutoCompleteItem = forwardRef<HTMLDivElement, any>(
|
||||
({ label, value, icon, url, ...others }: any, ref) => (
|
||||
@@ -121,11 +130,13 @@ export function SearchModuleComponent() {
|
||||
useEffect(() => {
|
||||
// Refresh the default search engine every time the config for it changes #521
|
||||
setSearchEngine(searchEnginesList[0]);
|
||||
}, [config.settings.searchUrl]);
|
||||
}, [searchEngineUrl]);
|
||||
const textInput = useRef<HTMLInputElement>(null);
|
||||
useHotkeys([['mod+K', () => textInput.current && textInput.current.focus()]]);
|
||||
useHotkeys([['mod+K', () => textInput.current?.focus()]]);
|
||||
const { classes } = useStyles();
|
||||
const openInNewTab = config.settings.searchNewTab ? '_blank' : '_self';
|
||||
const openInNewTab = config?.settings.common.searchEngine.properties.openInNewTab
|
||||
? '_blank'
|
||||
: '_self';
|
||||
const [OverseerrResults, setOverseerrResults] = useState<any[]>([]);
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
@@ -137,7 +148,7 @@ export function SearchModuleComponent() {
|
||||
}
|
||||
}, [debounced]);
|
||||
|
||||
const isModuleEnabled = config.modules?.[SearchModule.id]?.enabled ?? false;
|
||||
const isModuleEnabled = config?.settings.customization.layout.enabledSearchbar;
|
||||
if (!isModuleEnabled) {
|
||||
return null;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useMantineTheme } from '@mantine/core';
|
||||
|
||||
export const SafariStatusBarStyle = () => {
|
||||
const colorScheme = useMantineTheme();
|
||||
|
||||
const isDark = colorScheme.colorScheme === 'dark';
|
||||
|
||||
if (isDark) {
|
||||
return <meta name="apple-mobile-web-app-status-bar-style" content="white-translucent" />;
|
||||
}
|
||||
|
||||
return <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />;
|
||||
};
|
||||
22
src/components/layout/useCardStyles.ts
Normal file
22
src/components/layout/useCardStyles.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createStyles } from '@mantine/core';
|
||||
import { useConfigContext } from '../../config/provider';
|
||||
|
||||
export const useCardStyles = () => {
|
||||
const { config } = useConfigContext();
|
||||
const appOpacity = config?.settings.customization.appOpacity;
|
||||
return createStyles(({ colorScheme }, _params) => {
|
||||
const opacity = (appOpacity || 100) / 100;
|
||||
return {
|
||||
card: {
|
||||
backgroundColor:
|
||||
colorScheme === 'dark'
|
||||
? `rgba(37, 38, 43, ${opacity}) !important`
|
||||
: `rgba(255, 255, 255, ${opacity}) !important`,
|
||||
borderColor:
|
||||
colorScheme === 'dark'
|
||||
? `rgba(37, 38, 43, ${opacity})`
|
||||
: `rgba(233, 236, 239, ${opacity})`,
|
||||
},
|
||||
};
|
||||
})();
|
||||
};
|
||||
15
src/config/init.ts
Normal file
15
src/config/init.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useEffect } from 'react';
|
||||
import { ConfigType } from '../types/config';
|
||||
import { useConfigContext } from './provider';
|
||||
import { useConfigStore } from './store';
|
||||
|
||||
export const useInitConfig = (initialConfig: ConfigType) => {
|
||||
const { setConfigName } = useConfigContext();
|
||||
const configName = initialConfig.configProperties?.name ?? 'default';
|
||||
const initConfig = useConfigStore((x) => x.initConfig);
|
||||
|
||||
useEffect(() => {
|
||||
setConfigName(configName);
|
||||
initConfig(configName, initialConfig);
|
||||
}, [configName]);
|
||||
};
|
||||
45
src/config/provider.tsx
Normal file
45
src/config/provider.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
|
||||
import shallow from 'zustand/shallow';
|
||||
import { useColorTheme } from '../tools/color';
|
||||
import { ConfigType } from '../types/config';
|
||||
import { useConfigStore } from './store';
|
||||
|
||||
export type ConfigContextType = {
|
||||
config: ConfigType | undefined;
|
||||
name: string | undefined;
|
||||
setConfigName: (name: string) => void;
|
||||
};
|
||||
|
||||
const ConfigContext = createContext<ConfigContextType>({
|
||||
name: 'unknown',
|
||||
config: undefined,
|
||||
setConfigName: () => console.error('Provider not set'),
|
||||
});
|
||||
|
||||
export const ConfigProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [configName, setConfigName] = useState<string>();
|
||||
const { configs } = useConfigStore((s) => ({ configs: s.configs }), shallow);
|
||||
const { setPrimaryColor, setSecondaryColor, setPrimaryShade } = useColorTheme();
|
||||
|
||||
const currentConfig = configs.find((c) => c.configProperties.name === configName);
|
||||
|
||||
useEffect(() => {
|
||||
setPrimaryColor(currentConfig?.settings.customization.colors.primary || 'red');
|
||||
setSecondaryColor(currentConfig?.settings.customization.colors.secondary || 'orange');
|
||||
setPrimaryShade(currentConfig?.settings.customization.colors.shade || 6);
|
||||
}, [configName]);
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider
|
||||
value={{
|
||||
name: configName,
|
||||
config: currentConfig,
|
||||
setConfigName: (name: string) => setConfigName(name),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useConfigContext = () => useContext(ConfigContext);
|
||||
35
src/config/store.ts
Normal file
35
src/config/store.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import create from 'zustand';
|
||||
import { ConfigType } from '../types/config';
|
||||
|
||||
export const useConfigStore = create<UseConfigStoreType>((set, get) => ({
|
||||
configs: [],
|
||||
initConfig: (name, config) => {
|
||||
set((old) => ({
|
||||
...old,
|
||||
configs: [...old.configs.filter((x) => x.configProperties?.name !== name), config],
|
||||
}));
|
||||
},
|
||||
// TODO: use callback with current config as input
|
||||
updateConfig: async (name, updateCallback: (previous: ConfigType) => ConfigType) => {
|
||||
const { configs } = get();
|
||||
const currentConfig = configs.find((x) => x.configProperties.name === name);
|
||||
if (!currentConfig) return;
|
||||
|
||||
// TODO: update config on server
|
||||
const updatedConfig = updateCallback(currentConfig);
|
||||
|
||||
set((old) => ({
|
||||
...old,
|
||||
configs: [...old.configs.filter((x) => x.configProperties.name !== name), updatedConfig],
|
||||
}));
|
||||
},
|
||||
}));
|
||||
|
||||
interface UseConfigStoreType {
|
||||
configs: ConfigType[];
|
||||
initConfig: (name: string, config: ConfigType) => void;
|
||||
updateConfig: (
|
||||
name: string,
|
||||
updateCallback: (previous: ConfigType) => ConfigType
|
||||
) => Promise<void>;
|
||||
}
|
||||
23
src/hooks/use-resize.ts
Normal file
23
src/hooks/use-resize.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useCallback, useEffect, useState, MutableRefObject } from 'react';
|
||||
|
||||
export const useResize = (myRef: MutableRefObject<HTMLDivElement | null>) => {
|
||||
const [width, setWidth] = useState(0);
|
||||
const [height, setHeight] = useState(0);
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
setWidth(myRef.current?.offsetWidth ?? 0);
|
||||
setHeight(myRef.current?.offsetHeight ?? 0);
|
||||
}, [myRef]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('load', handleResize);
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('load', handleResize);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [myRef, handleResize]);
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
@@ -3,7 +3,6 @@ export * from './dashdot';
|
||||
export * from './date';
|
||||
export * from './torrents';
|
||||
export * from './ping';
|
||||
export * from './search';
|
||||
export * from './weather';
|
||||
export * from './docker';
|
||||
export * from './overseerr';
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { SearchModule } from './SearchModule';
|
||||
@@ -1,18 +1,18 @@
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
import { useState } from 'react';
|
||||
import { AppProps } from 'next/app';
|
||||
import { getCookie } from 'cookies-next';
|
||||
import Head from 'next/head';
|
||||
import { MantineProvider, ColorScheme, ColorSchemeProvider, MantineTheme } from '@mantine/core';
|
||||
import { NotificationsProvider } from '@mantine/notifications';
|
||||
import { ColorScheme, ColorSchemeProvider, MantineProvider, MantineTheme } from '@mantine/core';
|
||||
import { useColorScheme, useHotkeys, useLocalStorage } from '@mantine/hooks';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import { appWithTranslation } from 'next-i18next';
|
||||
import { NotificationsProvider } from '@mantine/notifications';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ConfigProvider } from '../tools/state';
|
||||
import { theme } from '../tools/theme';
|
||||
import { getCookie } from 'cookies-next';
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
import { appWithTranslation } from 'next-i18next';
|
||||
import { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import { useState } from 'react';
|
||||
import { ConfigProvider } from '../config/provider';
|
||||
import { ColorTheme } from '../tools/color';
|
||||
import { queryClient } from '../tools/queryClient';
|
||||
import { theme } from '../tools/theme';
|
||||
|
||||
function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
|
||||
const { Component, pageProps } = props;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { q } = req.query;
|
||||
const response = await axios.get(`https://duckduckgo.com/ac/?q=${q}`);
|
||||
res.status(200).json(response.data);
|
||||
}
|
||||
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
// Filter out if the reuqest is a POST or a GET
|
||||
if (req.method === 'GET') {
|
||||
return Get(req, res);
|
||||
}
|
||||
return res.status(405).json({
|
||||
statusCode: 405,
|
||||
message: 'Method not allowed',
|
||||
});
|
||||
};
|
||||
@@ -1,17 +1,15 @@
|
||||
import { getCookie, setCookie } from 'cookies-next';
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
import { useEffect } from 'react';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
|
||||
import AppShelf from '../components/AppShelf/AppShelf';
|
||||
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||
import { Config } from '../tools/types';
|
||||
import { useConfig } from '../tools/state';
|
||||
import { migrateToIdConfig } from '../tools/migrate';
|
||||
import { getConfig } from '../tools/getConfig';
|
||||
import { useColorTheme } from '../tools/color';
|
||||
import { Dashboard } from '../components/Dashboard/Dashboard';
|
||||
import Layout from '../components/layout/Layout';
|
||||
import { useInitConfig } from '../config/init';
|
||||
import { getConfig } from '../tools/getConfig';
|
||||
import { dashboardNamespaces } from '../tools/translation-namespaces';
|
||||
import { Config } from '../tools/types';
|
||||
import { ConfigType } from '../types/config';
|
||||
|
||||
export async function getServerSideProps({
|
||||
req,
|
||||
@@ -38,18 +36,20 @@ export async function getServerSideProps({
|
||||
}
|
||||
|
||||
export default function HomePage(props: any) {
|
||||
const { config: initialConfig }: { config: Config } = props;
|
||||
const { setConfig } = useConfig();
|
||||
const { config: initialConfig }: { config: ConfigType } = props;
|
||||
/*const { setConfig } = useConfig();
|
||||
const { setPrimaryColor, setSecondaryColor } = useColorTheme();
|
||||
useEffect(() => {
|
||||
const migratedConfig = migrateToIdConfig(initialConfig);
|
||||
setPrimaryColor(migratedConfig.settings.primaryColor || 'red');
|
||||
setSecondaryColor(migratedConfig.settings.secondaryColor || 'orange');
|
||||
setConfig(migratedConfig);
|
||||
}, [initialConfig]);
|
||||
}, [initialConfig]);*/
|
||||
useInitConfig(initialConfig);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<AppShelf />
|
||||
<Dashboard />
|
||||
<LoadConfigComponent />
|
||||
</Layout>
|
||||
);
|
||||
|
||||
22
src/types/area.ts
Normal file
22
src/types/area.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export type AreaType = WrapperAreaType | CategoryAreaType | SidebarAreaType;
|
||||
|
||||
interface WrapperAreaType {
|
||||
type: 'wrapper';
|
||||
properties: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CategoryAreaType {
|
||||
type: 'category';
|
||||
properties: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SidebarAreaType {
|
||||
type: 'sidebar';
|
||||
properties: {
|
||||
location: 'right' | 'left';
|
||||
};
|
||||
}
|
||||
5
src/types/category.ts
Normal file
5
src/types/category.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface CategoryType {
|
||||
id: string;
|
||||
position: number;
|
||||
name: string;
|
||||
}
|
||||
19
src/types/config.ts
Normal file
19
src/types/config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { CategoryType } from './category';
|
||||
import { WrapperType } from './wrapper';
|
||||
import { ServiceType } from './service';
|
||||
import { IntegrationsType } from './integration';
|
||||
import { SettingsType } from './settings';
|
||||
|
||||
export interface ConfigType {
|
||||
schemaVersion: string;
|
||||
configProperties: ConfigPropertiesType;
|
||||
categories: CategoryType[];
|
||||
wrappers: WrapperType[];
|
||||
services: ServiceType[];
|
||||
integrations: IntegrationsType;
|
||||
settings: SettingsType;
|
||||
}
|
||||
|
||||
export interface ConfigPropertiesType {
|
||||
name: string;
|
||||
}
|
||||
51
src/types/integration.ts
Normal file
51
src/types/integration.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { TileBaseType } from './tile';
|
||||
|
||||
export interface IntegrationsType {
|
||||
calendar?: CalendarIntegrationType;
|
||||
clock?: ClockIntegrationType;
|
||||
weather?: WeatherIntegrationType;
|
||||
dashDot?: DashDotIntegrationType;
|
||||
bitTorrent?: BitTorrentIntegrationType;
|
||||
useNet?: UseNetIntegrationType;
|
||||
torrentNetworkTraffic?: TorrentNetworkTrafficIntegrationType;
|
||||
}
|
||||
|
||||
export interface CalendarIntegrationType extends TileBaseType {
|
||||
properties: {
|
||||
isWeekStartingAtSunday: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ClockIntegrationType extends TileBaseType {
|
||||
properties: {
|
||||
is24HoursFormat: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WeatherIntegrationType extends TileBaseType {
|
||||
properties: {
|
||||
location: string;
|
||||
isFahrenheit: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DashDotIntegrationType extends TileBaseType {
|
||||
properties: {
|
||||
graphs: DashDotIntegrationGraphType[];
|
||||
isCompactView: boolean;
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
type DashDotIntegrationGraphType = { name: DashDotGraphType; isMultiView?: boolean };
|
||||
export type DashDotGraphType = 'cpu' | 'storage' | 'ram' | 'network' | 'gpu';
|
||||
|
||||
export interface BitTorrentIntegrationType extends TileBaseType {
|
||||
properties: {
|
||||
hideDownloadedTorrents: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UseNetIntegrationType extends TileBaseType {}
|
||||
|
||||
export interface TorrentNetworkTrafficIntegrationType extends TileBaseType {}
|
||||
54
src/types/service.ts
Normal file
54
src/types/service.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { TileBaseType } from './tile';
|
||||
|
||||
export interface ServiceType extends TileBaseType {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
behaviour: ServiceBehaviourType;
|
||||
network: ServiceNetworkType;
|
||||
appearance: ServiceAppearanceType;
|
||||
integration?: ServiceIntegrationType; //TODO: make this nullable
|
||||
}
|
||||
|
||||
interface ServiceBehaviourType {
|
||||
onClickUrl: string;
|
||||
isMoveable: boolean; //TODO: remove this proeprty
|
||||
isSticky: boolean; //TODO: remove this property
|
||||
isOpeningNewTab: boolean;
|
||||
}
|
||||
|
||||
interface ServiceNetworkType {
|
||||
enabledStatusChecker: boolean;
|
||||
okStatus: number[];
|
||||
}
|
||||
|
||||
interface ServiceAppearanceType {
|
||||
iconUrl: string;
|
||||
}
|
||||
|
||||
type ServiceIntegrationType =
|
||||
| ServiceIntegrationApiKeyType
|
||||
| ServiceIntegrationPasswordType
|
||||
| ServiceIntegrationUsernamePasswordType;
|
||||
|
||||
export interface ServiceIntegrationApiKeyType {
|
||||
type: 'readarr' | 'radarr' | 'sonarr' | 'lidarr' | 'sabnzbd' | 'jellyseerr' | 'overseerr';
|
||||
properties: {
|
||||
apiKey: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ServiceIntegrationPasswordType {
|
||||
type: 'deluge';
|
||||
properties: {
|
||||
password?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ServiceIntegrationUsernamePasswordType {
|
||||
type: 'qBittorrent' | 'transmission';
|
||||
properties: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
}
|
||||
61
src/types/settings.ts
Normal file
61
src/types/settings.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { MantineTheme } from '@mantine/core';
|
||||
|
||||
export interface SettingsType {
|
||||
common: CommonSettingsType;
|
||||
customization: CustomizationSettingsType;
|
||||
}
|
||||
|
||||
export interface CommonSettingsType {
|
||||
searchEngine: SearchEngineCommonSettingsType;
|
||||
defaultConfig: string;
|
||||
}
|
||||
|
||||
export type SearchEngineCommonSettingsType =
|
||||
| CommonSearchEngineCommonSettingsType
|
||||
| CustomSearchEngineCommonSettingsType;
|
||||
|
||||
export interface CommonSearchEngineCommonSettingsType extends BaseSearchEngineType {
|
||||
type: 'google' | 'duckDuckGo' | 'bing';
|
||||
}
|
||||
|
||||
interface CustomSearchEngineCommonSettingsType extends BaseSearchEngineType {
|
||||
type: 'custom';
|
||||
properties: {
|
||||
template: string;
|
||||
openInNewTab: boolean;
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface BaseSearchEngineType {
|
||||
properties: {
|
||||
openInNewTab: boolean;
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CustomizationSettingsType {
|
||||
layout: LayoutCustomizationSettingsType;
|
||||
pageTitle?: string;
|
||||
metaTitle?: string;
|
||||
logoImageUrl?: string;
|
||||
faviconUrl?: string;
|
||||
backgroundImageUrl?: string;
|
||||
customCss?: string;
|
||||
colors: ColorsCustomizationSettingsType;
|
||||
appOpacity?: number;
|
||||
}
|
||||
|
||||
interface LayoutCustomizationSettingsType {
|
||||
enabledLeftSidebar: boolean;
|
||||
enabledRightSidebar: boolean;
|
||||
enabledDocker: boolean;
|
||||
enabledPing: boolean;
|
||||
enabledSearchbar: boolean;
|
||||
}
|
||||
|
||||
interface ColorsCustomizationSettingsType {
|
||||
primary?: MantineTheme['primaryColor'];
|
||||
secondary?: MantineTheme['primaryColor'];
|
||||
shade?: MantineTheme['primaryShade'];
|
||||
}
|
||||
10
src/types/shape.ts
Normal file
10
src/types/shape.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface ShapeType {
|
||||
location: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
size: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
7
src/types/tile.ts
Normal file
7
src/types/tile.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AreaType } from './area';
|
||||
import { ShapeType } from './shape';
|
||||
|
||||
export interface TileBaseType {
|
||||
area: AreaType;
|
||||
shape: ShapeType;
|
||||
}
|
||||
4
src/types/wrapper.ts
Normal file
4
src/types/wrapper.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface WrapperType {
|
||||
id: string;
|
||||
position: number;
|
||||
}
|
||||
252
yarn.lock
252
yarn.lock
@@ -1092,163 +1092,163 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/carousel@npm:^5.1.0":
|
||||
version: 5.1.0
|
||||
resolution: "@mantine/carousel@npm:5.1.0"
|
||||
"@mantine/carousel@npm:^5.9.0":
|
||||
version: 5.9.0
|
||||
resolution: "@mantine/carousel@npm:5.9.0"
|
||||
dependencies:
|
||||
"@mantine/utils": 5.1.0
|
||||
"@mantine/utils": 5.9.0
|
||||
peerDependencies:
|
||||
"@mantine/core": 5.1.0
|
||||
"@mantine/hooks": 5.1.0
|
||||
embla-carousel-react: 7.0.0
|
||||
"@mantine/core": 5.9.0
|
||||
"@mantine/hooks": 5.9.0
|
||||
embla-carousel-react: ^7.0.0
|
||||
react: ">=16.8.0"
|
||||
checksum: 0aa6beda2c1c406ea1cf07ece919999a7591b838579c1a0b90f88d1491d75f0871418deb700976791e14d870543f7594ea28569af13b330444ed8c810ab47ac3
|
||||
checksum: 6a69fbbaeafcbafce38c562010c36dac9d88b374d2c04effbf9656aade161c25026ce68b3b11e893478f321a1321acadead01c9e82ec8ac23e86b1656f878c76
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/core@npm:^5.7.2":
|
||||
version: 5.7.2
|
||||
resolution: "@mantine/core@npm:5.7.2"
|
||||
"@mantine/core@npm:^5.9.0":
|
||||
version: 5.9.0
|
||||
resolution: "@mantine/core@npm:5.9.0"
|
||||
dependencies:
|
||||
"@floating-ui/react-dom-interactions": ^0.10.1
|
||||
"@mantine/styles": 5.7.2
|
||||
"@mantine/utils": 5.7.2
|
||||
"@mantine/styles": 5.9.0
|
||||
"@mantine/utils": 5.9.0
|
||||
"@radix-ui/react-scroll-area": 1.0.0
|
||||
react-textarea-autosize: 8.3.4
|
||||
peerDependencies:
|
||||
"@mantine/hooks": 5.7.2
|
||||
"@mantine/hooks": 5.9.0
|
||||
react: ">=16.8.0"
|
||||
react-dom: ">=16.8.0"
|
||||
checksum: 54a5460d4367179d4016068e6cdfdd6d6776921ff8e0c24926881564655ec0255588a842e166fb754685bf31fb485160a1759d07e0435a5620307b771dce65c3
|
||||
checksum: 65cf716d890a3bfb6126b090addcd1fcf276fdfbaf808ad00f8092dda0606b4e60734f179563357b88317cdf834a14efc518ad826742ad77d52b96f63f889b08
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/dates@npm:^5.7.2":
|
||||
version: 5.7.2
|
||||
resolution: "@mantine/dates@npm:5.7.2"
|
||||
"@mantine/dates@npm:^5.9.0":
|
||||
version: 5.9.0
|
||||
resolution: "@mantine/dates@npm:5.9.0"
|
||||
dependencies:
|
||||
"@mantine/utils": 5.7.2
|
||||
"@mantine/utils": 5.9.0
|
||||
peerDependencies:
|
||||
"@mantine/core": 5.7.2
|
||||
"@mantine/hooks": 5.7.2
|
||||
"@mantine/core": 5.9.0
|
||||
"@mantine/hooks": 5.9.0
|
||||
dayjs: ">=1.0.0"
|
||||
react: ">=16.8.0"
|
||||
checksum: 76362eba6e51ab95e2343482203598a17b62aad510620cc5bf7f3a703b808c0ded61e995c70df80d09a1c36e15f76acc66dd589b9e3b0d32f1bc5246c705be31
|
||||
checksum: 04619233b27c0cda8cbe2b2ba6da8ac27dbe419bda38bb411c2e3ad3fcf7f39d35ac84cb37861362e8d7d00de5819621176f389156da84a29ae84d0508dbd9f9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/dropzone@npm:^5.7.2":
|
||||
version: 5.7.2
|
||||
resolution: "@mantine/dropzone@npm:5.7.2"
|
||||
"@mantine/dropzone@npm:^5.9.0":
|
||||
version: 5.9.0
|
||||
resolution: "@mantine/dropzone@npm:5.9.0"
|
||||
dependencies:
|
||||
"@mantine/utils": 5.7.2
|
||||
"@mantine/utils": 5.9.0
|
||||
react-dropzone: 14.2.3
|
||||
peerDependencies:
|
||||
"@mantine/core": 5.7.2
|
||||
"@mantine/hooks": 5.7.2
|
||||
"@mantine/core": 5.9.0
|
||||
"@mantine/hooks": 5.9.0
|
||||
react: ">=16.8.0"
|
||||
react-dom: ">=16.8.0"
|
||||
checksum: 604c0adb49ee4939c739f19ec86ff1dbc27d59b46fc13f3ab2ff5cebd7cd6eae39936f54760db362d95a70a0b9b2c89af558a2acbd26ef1fa36b20a67803be5f
|
||||
checksum: 6234692b6080ab1bafc26ed32787ffbef2cc72b066d47561199dcbad9cd8d653d86f46aa59817f46b50de81db33c373cd39c9b5e4c6f5b0add2f5d67bacf20a8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/form@npm:^5.7.2":
|
||||
version: 5.7.2
|
||||
resolution: "@mantine/form@npm:5.7.2"
|
||||
"@mantine/form@npm:^5.9.0":
|
||||
version: 5.9.0
|
||||
resolution: "@mantine/form@npm:5.9.0"
|
||||
dependencies:
|
||||
fast-deep-equal: ^3.1.3
|
||||
klona: ^2.0.5
|
||||
peerDependencies:
|
||||
react: ">=16.8.0"
|
||||
checksum: 34d236411a0fbe32c194a2f5157246854446380b41e970f341703137d1422a6c43ae307175dd4c0d2c0512541e42aa1636ecd56d5550d6714c51f1f4890a592c
|
||||
checksum: 33eb92b4e9e52e547365719a785281e13de3843b5cfa470f12cb33aabcb1603453ab1b04280e8d2f81e726a6f70677eaae4ed6e19e9d0eb7af74f250119e917b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/hooks@npm:^5.7.2":
|
||||
version: 5.7.2
|
||||
resolution: "@mantine/hooks@npm:5.7.2"
|
||||
"@mantine/hooks@npm:^5.9.0":
|
||||
version: 5.9.0
|
||||
resolution: "@mantine/hooks@npm:5.9.0"
|
||||
peerDependencies:
|
||||
react: ">=16.8.0"
|
||||
checksum: 85977a0f3968be3ab5556e79cd378f210be3ffa0b4b3c59e995848ec05d8a8c9c340d2bfcbd1900fe05495a8c2a977653233f793a304984f5dc1f626d4d6b386
|
||||
checksum: ab49260ae8ce511849bc968b0ae087c44a4789f43ffb47c24658413cb8f179cef897429b34a7d4fbdb8e9b0d40d6eb29f51f4a91de086aaea7c5b894fe5ad21c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/modals@npm:^5.7.2":
|
||||
version: 5.7.2
|
||||
resolution: "@mantine/modals@npm:5.7.2"
|
||||
"@mantine/modals@npm:^5.9.0":
|
||||
version: 5.9.0
|
||||
resolution: "@mantine/modals@npm:5.9.0"
|
||||
dependencies:
|
||||
"@mantine/utils": 5.7.2
|
||||
"@mantine/utils": 5.9.0
|
||||
peerDependencies:
|
||||
"@mantine/core": 5.7.2
|
||||
"@mantine/hooks": 5.7.2
|
||||
"@mantine/core": 5.9.0
|
||||
"@mantine/hooks": 5.9.0
|
||||
react: ">=16.8.0"
|
||||
react-dom: ">=16.8.0"
|
||||
checksum: f233d6aa976e519bde789534801395c1f9d3e84c14893b992ffef4e3ebcde5aa4b2b0360d4b33b98681992a72ce13fdc0f7591afd80bb58accb3c63260a07fa8
|
||||
checksum: e522ace86deafc60d750c006a25d6c3caa1d0000e70d5d31365875b97daa3d24b0438827b9da2f1279cb38b2ae2ecaad2f895d185b1d6b4f4974041d4a7938bf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/next@npm:^5.2.3":
|
||||
version: 5.2.3
|
||||
resolution: "@mantine/next@npm:5.2.3"
|
||||
"@mantine/next@npm:^5.9.0":
|
||||
version: 5.9.0
|
||||
resolution: "@mantine/next@npm:5.9.0"
|
||||
dependencies:
|
||||
"@mantine/ssr": 5.2.3
|
||||
"@mantine/styles": 5.2.3
|
||||
"@mantine/ssr": 5.9.0
|
||||
"@mantine/styles": 5.9.0
|
||||
peerDependencies:
|
||||
next: "*"
|
||||
react: ">=16.8.0"
|
||||
react-dom: ">=16.8.0"
|
||||
checksum: bd77c1f8c4a2605ab3a16436b33685b93fb5c91bd82984868fbce43c4566b3a28c00e80adc5f9a454ada6614e5f1c8aa948f64871eab534468881ff47d86bbd7
|
||||
checksum: 20e1c1c2fe55c64973a0d92082b68f1edfa4fd5d157aa37c6562c31f4281cbb946e8e94505509e0c631dd9a74ad85d31eb165560f8117ae06c0466eed2153aac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/notifications@npm:^5.7.2":
|
||||
version: 5.7.2
|
||||
resolution: "@mantine/notifications@npm:5.7.2"
|
||||
"@mantine/notifications@npm:^5.9.0":
|
||||
version: 5.9.0
|
||||
resolution: "@mantine/notifications@npm:5.9.0"
|
||||
dependencies:
|
||||
"@mantine/utils": 5.7.2
|
||||
"@mantine/utils": 5.9.0
|
||||
react-transition-group: 4.4.2
|
||||
peerDependencies:
|
||||
"@mantine/core": 5.7.2
|
||||
"@mantine/hooks": 5.7.2
|
||||
"@mantine/core": 5.9.0
|
||||
"@mantine/hooks": 5.9.0
|
||||
react: ">=16.8.0"
|
||||
react-dom: ">=16.8.0"
|
||||
checksum: db1a8e343bda01dec80a9eb336daaa183ea73b8a8eed20cf80a5ef15b620f79488041dd492a68bd7460c0d9b2432a2f71ccc049bc44894c4c8837c5c078a5c3e
|
||||
checksum: b9428bfa1bd4ad95b9e670c5f7a2493e4b0ce5ef16d768cead1fb6ba7433e5cadc1e1e8e4aa8f755cacbeb317f41a00c666a5a99212b30356b4f828533f4b54d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/prism@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "@mantine/prism@npm:5.0.0"
|
||||
"@mantine/prism@npm:^5.9.0":
|
||||
version: 5.9.0
|
||||
resolution: "@mantine/prism@npm:5.9.0"
|
||||
dependencies:
|
||||
"@mantine/utils": 5.0.0
|
||||
"@mantine/utils": 5.9.0
|
||||
prism-react-renderer: ^1.2.1
|
||||
peerDependencies:
|
||||
"@mantine/core": 5.0.0
|
||||
"@mantine/hooks": 5.0.0
|
||||
"@mantine/core": 5.9.0
|
||||
"@mantine/hooks": 5.9.0
|
||||
react: ">=16.8.0"
|
||||
react-dom: ">=16.8.0"
|
||||
checksum: 2d0420424b0f0b32bb299cb15a856e97edb941fdc0bfba93508e9ce71e99a50a37a5d24951f48d04d0d10754bd4834d1247345456c2f34876f4fe3e106b773c1
|
||||
checksum: 6966b8f5f4e6212384c5fcc2789d7d8ccf7cfab08db7a1a932656c13084d54791ebe9b99523964979899bf7b441634f99819d07e6a3065d1899194571933a597
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/ssr@npm:5.2.3":
|
||||
version: 5.2.3
|
||||
resolution: "@mantine/ssr@npm:5.2.3"
|
||||
"@mantine/ssr@npm:5.9.0":
|
||||
version: 5.9.0
|
||||
resolution: "@mantine/ssr@npm:5.9.0"
|
||||
dependencies:
|
||||
"@mantine/styles": 5.2.3
|
||||
"@mantine/styles": 5.9.0
|
||||
html-react-parser: 1.4.12
|
||||
peerDependencies:
|
||||
"@emotion/react": ">=11.9.0"
|
||||
"@emotion/server": ">=11.4.0"
|
||||
react: ">=16.8.0"
|
||||
react-dom: ">=16.8.0"
|
||||
checksum: f9212233489b3d88198756646db6013fe1e9dce62619254431c5f029a3ff61da057993daecf10ffafe55f14f75ac78f85d0efbdacfff50f34df492bb13e81413
|
||||
checksum: 235549f738d6830c70b03cb6fe4255669273bbac7c694e9f8c0454ec30bdcc11e2e4fdec77e5a748412bedab664fc6ce5d39000187ecaa35f7e7016e9599e2cd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/styles@npm:5.2.3":
|
||||
version: 5.2.3
|
||||
resolution: "@mantine/styles@npm:5.2.3"
|
||||
"@mantine/styles@npm:5.9.0":
|
||||
version: 5.9.0
|
||||
resolution: "@mantine/styles@npm:5.9.0"
|
||||
dependencies:
|
||||
clsx: 1.1.1
|
||||
csstype: 3.0.9
|
||||
@@ -1256,48 +1256,16 @@ __metadata:
|
||||
"@emotion/react": ">=11.9.0"
|
||||
react: ">=16.8.0"
|
||||
react-dom: ">=16.8.0"
|
||||
checksum: 39e9e3c7462386da8106260787e0537b767e54b8b01b5077b3e371c9026ad78228e8bea801d4d3968faca6254e509bdd3ca783eba8a62be0d011f585bdf6019e
|
||||
checksum: 5b28f0ad42c5595714869a16b632cd5997c877a4c3c6d13c5ecde0f5c3d7be2eac659e185b207f4935b558f9a084ce4882f5e0342699baa62ede688028e0eab5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/styles@npm:5.7.2":
|
||||
version: 5.7.2
|
||||
resolution: "@mantine/styles@npm:5.7.2"
|
||||
dependencies:
|
||||
clsx: 1.1.1
|
||||
csstype: 3.0.9
|
||||
peerDependencies:
|
||||
"@emotion/react": ">=11.9.0"
|
||||
react: ">=16.8.0"
|
||||
react-dom: ">=16.8.0"
|
||||
checksum: d440f92b830e232b4826e0ff8002f4821d5eee0f379d8e9e5cea9346ca550719b89aa850d5d06f0feebd6150172a398136391285d57e7174180bfa8294bbba65
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/utils@npm:5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "@mantine/utils@npm:5.0.0"
|
||||
"@mantine/utils@npm:5.9.0":
|
||||
version: 5.9.0
|
||||
resolution: "@mantine/utils@npm:5.9.0"
|
||||
peerDependencies:
|
||||
react: ">=16.8.0"
|
||||
checksum: 7d167dd40e02e6f2cb830ede1aad091af7bf9eb42fa1b47a08451286e98d3fa1a88f49a3e179d7969be450f70fc3d02f9851b8655d310499fa6bc431978ed232
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/utils@npm:5.1.0":
|
||||
version: 5.1.0
|
||||
resolution: "@mantine/utils@npm:5.1.0"
|
||||
peerDependencies:
|
||||
react: ">=16.8.0"
|
||||
checksum: f6d2dd28f97d9e2d09eea3db7d1f2e0a82c451bd7a0c80f9a38c421c7003052b822917d4128a055e39c5822c6de61ecb53728aa86ab726bf4dcda911ab47f650
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mantine/utils@npm:5.7.2":
|
||||
version: 5.7.2
|
||||
resolution: "@mantine/utils@npm:5.7.2"
|
||||
peerDependencies:
|
||||
react: ">=16.8.0"
|
||||
checksum: 35ce46a03a24f8f2b649b833b725120e50e639a66a3a9b399a10f5c4083446a4338ececd8b1d6f0897743cb1bc6d6757c7a530c515f032c0ab23b4f8e9d04b2a
|
||||
checksum: 5fbc8a22652b1e7aeea454b1880881427acb12025a2caea6b2d7a3abe99dd06220b9a1fd28d4e6eaef1978dab6c44cf50be82078afde0fbade4e57b4d4a26675
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1941,9 +1909,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tabler/icons@npm:^1.78.0":
|
||||
version: 1.78.0
|
||||
resolution: "@tabler/icons@npm:1.78.0"
|
||||
"@tabler/icons@npm:^1.106.0":
|
||||
version: 1.115.0
|
||||
resolution: "@tabler/icons@npm:1.115.0"
|
||||
peerDependencies:
|
||||
react: ^16.x || 17.x || 18.x
|
||||
react-dom: ^16.x || 17.x || 18.x
|
||||
@@ -1952,7 +1920,7 @@ __metadata:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
checksum: f3789c4681fc7a3520585522afd8306d18ab3ec49077687b956c837f0d088b5c2a68c1bc90ff44f3369e9a5b3f4b4501f1a50133e8dff93e0947cdd53aceefea
|
||||
checksum: fb2cf8b49d2f6131ee5c8b962ad51cad57b2313dc5d10de82444a1442fa276e1b826b8ed253beab349dd01805fb9febc3546ccaa4d2c955c2f2556e5e154a6af
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2414,13 +2382,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"add@npm:^2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "add@npm:2.0.6"
|
||||
checksum: e2d23d40494565dfed4acd65e478570c444db5ac6c053551ed429c39ea0f2c99d83df63e7befec936df601827d2254d06a2fb6f7dcfd2022e810b25eab818b8c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"agent-base@npm:6, agent-base@npm:^6.0.2":
|
||||
version: 6.0.2
|
||||
resolution: "agent-base@npm:6.0.2"
|
||||
@@ -4329,6 +4290,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fily-publish-gridstack@npm:^0.0.13":
|
||||
version: 0.0.13
|
||||
resolution: "fily-publish-gridstack@npm:0.0.13"
|
||||
checksum: e025e32982397e0cd8d37f022a39911172579bec984a7cf05c10ee06b9b8a51471820c74b95f2dc9bde97af98e305527545955ad4ca6cc89b830c864f91bb755
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"find-root@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "find-root@npm:1.1.0"
|
||||
@@ -4832,21 +4800,21 @@ __metadata:
|
||||
"@dnd-kit/utilities": ^3.2.0
|
||||
"@emotion/react": ^11.10.5
|
||||
"@emotion/server": ^11.10.0
|
||||
"@mantine/carousel": ^5.1.0
|
||||
"@mantine/core": ^5.7.2
|
||||
"@mantine/dates": ^5.7.2
|
||||
"@mantine/dropzone": ^5.7.2
|
||||
"@mantine/form": ^5.7.2
|
||||
"@mantine/hooks": ^5.7.2
|
||||
"@mantine/modals": ^5.7.2
|
||||
"@mantine/next": ^5.2.3
|
||||
"@mantine/notifications": ^5.7.2
|
||||
"@mantine/prism": ^5.0.0
|
||||
"@mantine/carousel": ^5.9.0
|
||||
"@mantine/core": ^5.9.0
|
||||
"@mantine/dates": ^5.9.0
|
||||
"@mantine/dropzone": ^5.9.0
|
||||
"@mantine/form": ^5.9.0
|
||||
"@mantine/hooks": ^5.9.0
|
||||
"@mantine/modals": ^5.9.0
|
||||
"@mantine/next": ^5.9.0
|
||||
"@mantine/notifications": ^5.9.0
|
||||
"@mantine/prism": ^5.9.0
|
||||
"@next/bundle-analyzer": ^12.1.4
|
||||
"@next/eslint-plugin-next": ^12.1.4
|
||||
"@nivo/core": ^0.79.0
|
||||
"@nivo/line": ^0.79.1
|
||||
"@tabler/icons": ^1.78.0
|
||||
"@tabler/icons": ^1.106.0
|
||||
"@tanstack/react-query": ^4.2.1
|
||||
"@types/dockerode": ^3.3.9
|
||||
"@types/node": 17.0.1
|
||||
@@ -4855,7 +4823,6 @@ __metadata:
|
||||
"@types/uuid": ^8.3.4
|
||||
"@typescript-eslint/eslint-plugin": ^5.30.7
|
||||
"@typescript-eslint/parser": ^5.30.7
|
||||
add: ^2.0.6
|
||||
axios: ^0.27.2
|
||||
consola: ^2.15.3
|
||||
cookies-next: ^2.1.1
|
||||
@@ -4873,6 +4840,7 @@ __metadata:
|
||||
eslint-plugin-react-hooks: ^4.6.0
|
||||
eslint-plugin-testing-library: ^5.5.1
|
||||
eslint-plugin-unused-imports: ^2.0.0
|
||||
fily-publish-gridstack: ^0.0.13
|
||||
framer-motion: ^6.5.1
|
||||
i18next: ^21.9.1
|
||||
i18next-browser-languagedetector: ^6.1.5
|
||||
@@ -4893,6 +4861,7 @@ __metadata:
|
||||
typescript: ^4.7.4
|
||||
uuid: ^8.3.2
|
||||
yarn: ^1.22.19
|
||||
zustand: ^4.1.4
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -8295,7 +8264,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-sync-external-store@npm:^1.2.0":
|
||||
"use-sync-external-store@npm:1.2.0, use-sync-external-store@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "use-sync-external-store@npm:1.2.0"
|
||||
peerDependencies:
|
||||
@@ -8555,3 +8524,20 @@ __metadata:
|
||||
checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zustand@npm:^4.1.4":
|
||||
version: 4.1.4
|
||||
resolution: "zustand@npm:4.1.4"
|
||||
dependencies:
|
||||
use-sync-external-store: 1.2.0
|
||||
peerDependencies:
|
||||
immer: ">=9.0"
|
||||
react: ">=16.8"
|
||||
peerDependenciesMeta:
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
checksum: a9ceb7849ebf407d31a6121be09acfb041324f6f45ca9a3f62ab85e2d840d0f48f157abd99ed7b9e08b96ebb4e15480b4a196ed8b1df8a5a9a2ba9afd7c639a7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
Reference in New Issue
Block a user