diff --git a/package.json b/package.json index 8f72efb94..6307f33e6 100644 --- a/package.json +++ b/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", diff --git a/public/locales/en/settings/customization/page-appearance.json b/public/locales/en/settings/customization/page-appearance.json index 63fa96886..2a1e0424b 100644 --- a/public/locales/en/settings/customization/page-appearance.json +++ b/public/locales/en/settings/customization/page-appearance.json @@ -1,6 +1,10 @@ { "pageTitle": { "label": "Page Title", + "placeholder": "Homarr" + }, + "metaTitle": { + "label": "Meta Title", "placeholder": "Homarr 🦞" }, "logo": { diff --git a/public/locales/en/settings/general/config-changer.json b/public/locales/en/settings/general/config-changer.json index ad4ac012d..ccd79607a 100644 --- a/public/locales/en/settings/general/config-changer.json +++ b/public/locales/en/settings/general/config-changer.json @@ -7,6 +7,9 @@ "form": { "configName": { "label": "Config name", + "validation": { + "required": "Config name is required" + }, "placeholder": "Your new config name" }, "submitButton": "Confirm" diff --git a/public/locales/en/settings/general/module-enabler.json b/public/locales/en/settings/general/module-enabler.json index 179753b6f..26c1dae88 100644 --- a/public/locales/en/settings/general/module-enabler.json +++ b/public/locales/en/settings/general/module-enabler.json @@ -1,3 +1,3 @@ { - "title": "Module enabler" + "title": "Enabled modules" } \ No newline at end of file diff --git a/public/locales/en/settings/general/search-engine.json b/public/locales/en/settings/general/search-engine.json index 8d419fcf8..fa9ae412f 100644 --- a/public/locales/en/settings/general/search-engine.json +++ b/public/locales/en/settings/general/search-engine.json @@ -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" } } \ No newline at end of file diff --git a/src/components/Config/ConfigChanger.tsx b/src/components/Config/ConfigChanger.tsx index 0e129bd2c..666e1e487 100644 --- a/src/components/Config/ConfigChanger.tsx +++ b/src/components/Config/ConfigChanger.tsx @@ -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([]); - 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 (
@@ -23,23 +36,22 @@ export default function ConfigChanger() { ); } - // return { - 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[]; diff --git a/src/components/Config/SaveConfig.tsx b/src/components/Config/SaveConfig.tsx deleted file mode 100644 index 327f50523..000000000 --- a/src/components/Config/SaveConfig.tsx +++ /dev/null @@ -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 ( - - setOpened(false)} title={t('modal.title')}> -
{ - setConfig({ ...config, name: values.configName }); - setOpened(false); - showNotification({ - title: t('modal.events.configSaved.title'), - icon: , - color: 'green', - autoClose: 1500, - radius: 'md', - message: t('modal.events.configSaved.message', { configName: values.configName }), - }); - })} - > - - - - - -
- - - -
- ); -} diff --git a/src/components/Dashboard/Dashboard.tsx b/src/components/Dashboard/Dashboard.tsx new file mode 100644 index 000000000..8bde2af95 --- /dev/null +++ b/src/components/Dashboard/Dashboard.tsx @@ -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 ? : } + + ); +}; diff --git a/src/components/Dashboard/Tiles/Service/Service.tsx b/src/components/Dashboard/Tiles/Service/Service.tsx new file mode 100644 index 000000000..1f326aff8 --- /dev/null +++ b/src/components/Dashboard/Tiles/Service/Service.tsx @@ -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 = ( + <> + + {service.name} + +
+ +
+ + ); + + return ( + + {isEditMode && + { + /**/ + }}{' '} + {/* TODO: change to serviceMenu */} + {!service.url || isEditMode ? ( + + {inner} + + ) : ( + + {inner} + + )} + {/**/} + + ); +}; + +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 + }, + }, + }; +}); diff --git a/src/components/Dashboard/Tiles/TileWrapper.tsx b/src/components/Dashboard/Tiles/TileWrapper.tsx new file mode 100644 index 000000000..9c2a89cda --- /dev/null +++ b/src/components/Dashboard/Tiles/TileWrapper.tsx @@ -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; + children: ReactNode; +} + +export const GridstackTileWrapper = ({ + id, + type, + x, + y, + width, + height, + minWidth, + minHeight, + maxWidth, + maxHeight, + children, + itemRef, +}: GridstackTileWrapperProps) => ( +
+ {children} +
+); diff --git a/src/components/Dashboard/Tiles/definition.tsx b/src/components/Dashboard/Tiles/definition.tsx new file mode 100644 index 000000000..48e249779 --- /dev/null +++ b/src/components/Dashboard/Tiles/definition.tsx @@ -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, + }, +}; diff --git a/src/components/Dashboard/Tiles/type.ts b/src/components/Dashboard/Tiles/type.ts new file mode 100644 index 000000000..e5e860af9 --- /dev/null +++ b/src/components/Dashboard/Tiles/type.ts @@ -0,0 +1,3 @@ +export interface BaseTileProps { + className?: string; +} diff --git a/src/components/Dashboard/Views/DetailView.tsx b/src/components/Dashboard/Views/DetailView.tsx new file mode 100644 index 000000000..ff7f93168 --- /dev/null +++ b/src/components/Dashboard/Views/DetailView.tsx @@ -0,0 +1,5 @@ +import { DashboardView } from './main'; + +export const DashboardDetailView = () => { + return ; +}; diff --git a/src/components/Dashboard/Views/EditView.tsx b/src/components/Dashboard/Views/EditView.tsx new file mode 100644 index 000000000..1d9689152 --- /dev/null +++ b/src/components/Dashboard/Views/EditView.tsx @@ -0,0 +1,5 @@ +import { DashboardView } from './main'; + +export const DashboardEditView = () => { + return ; +}; diff --git a/src/components/Dashboard/Views/main.tsx b/src/components/Dashboard/Views/main.tsx new file mode 100644 index 000000000..ea327713c --- /dev/null +++ b/src/components/Dashboard/Views/main.tsx @@ -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 ( + + {/**/} + + {wrappers.map( + (item) => + item.type === 'category' + ? 'category' // + : 'wrapper' // + )} + + {/**/} + + ); +}; + +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] + ); +}; diff --git a/src/components/Dashboard/Views/store.ts b/src/components/Dashboard/Views/store.ts new file mode 100644 index 000000000..5c9fbafc2 --- /dev/null +++ b/src/components/Dashboard/Views/store.ts @@ -0,0 +1,11 @@ +import create from 'zustand'; + +interface EditModeState { + enabled: boolean; + toggleEditMode: () => void; +} + +export const useEditModeStore = create((set) => ({ + enabled: false, + toggleEditMode: () => set((state) => ({ enabled: !state.enabled })), +})); diff --git a/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx b/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx new file mode 100644 index 000000000..fc11cc99f --- /dev/null +++ b/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx @@ -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 ( + +
+ {items.map((service) => { + const { component: TileComponent, ...tile } = Tiles['service']; + return ( + + + + ); + })} + {Object.entries(integrations).map(([k, v]) => { + const { component: TileComponent, ...tile } = Tiles[k as keyof typeof Tiles]; + + return ( + + + + ); + })} +
+
+ ); +}; + +const useMinRowForFullHeight = (wrapperRef: RefObject) => { + return wrapperRef.current ? Math.floor(wrapperRef.current!.offsetHeight / 64) : 2; +}; diff --git a/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts b/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts new file mode 100644 index 000000000..646562ba4 --- /dev/null +++ b/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts @@ -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, + gridRef: MutableRefObject, + itemRefs: MutableRefObject>>, + 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); +}; diff --git a/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts b/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts new file mode 100644 index 000000000..5f75d1adb --- /dev/null +++ b/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts @@ -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; + refs: { + wrapper: RefObject; + items: MutableRefObject>>; + gridstack: MutableRefObject; + }; +} + +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(null); + // references to the diffrent items contained in the gridstack + const itemRefs = useRef>>({}); + // reference of the gridstack object for modifications after initialization + const gridRef = useRef(); + // 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, + }, + }; +}; diff --git a/src/components/SearchNewTabSwitch/SearchNewTabSwitch.tsx b/src/components/SearchNewTabSwitch/SearchNewTabSwitch.tsx deleted file mode 100644 index 00ebb88f4..000000000 --- a/src/components/SearchNewTabSwitch/SearchNewTabSwitch.tsx +++ /dev/null @@ -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(defaultPosition); - const { t } = useTranslation('settings/general/search-engine'); - const toggleOpenInNewTab = () => { - setOpenInNewTab(!openInNewTab); - setConfig({ - ...config, - settings: { - ...config.settings, - searchNewTab: !openInNewTab, - }, - }); - }; - - return ( - -
- toggleOpenInNewTab()} size="md" /> -
- {t('searchNewTab.label')} -
- ); -} diff --git a/src/components/Settings/AdvancedSettings.tsx b/src/components/Settings/AdvancedSettings.tsx deleted file mode 100644 index 3d5b7bd96..000000000 --- a/src/components/Settings/AdvancedSettings.tsx +++ /dev/null @@ -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 ( - -
saveChanges(values))}> - - - - - -