diff --git a/components/AppShelf/AddAppShelfItem.tsx b/components/AppShelf/AddAppShelfItem.tsx index 37eac451e..5f42d2af2 100644 --- a/components/AppShelf/AddAppShelfItem.tsx +++ b/components/AppShelf/AddAppShelfItem.tsx @@ -1,5 +1,4 @@ import { - useMantineTheme, Modal, Center, Group, @@ -21,9 +20,7 @@ import { ServiceTypeList } from '../../tools/types'; import { AppShelfItemWrapper } from './AppShelfItemWrapper'; export default function AddItemShelfItem(props: any) { - const { addService } = useConfig(); const [opened, setOpened] = useState(false); - const theme = useMantineTheme(); return ( <> { + .catch(() => { // Do nothing }); @@ -92,7 +89,7 @@ function MatchIcon(name: string, form: any) { export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & any) { const { setOpened } = props; - const { addService, config, setConfig } = useConfig(); + const { config, setConfig } = useConfig(); const [isLoading, setLoading] = useState(false); const form = useForm({ @@ -104,7 +101,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & apiKey: props.apiKey ?? (undefined as unknown as string), }, validate: { - apiKey: (value: string) => null, + apiKey: () => null, // Validate icon with a regex icon: (value: string) => { if (!value.match(/^https?:\/\/.+\.(png|jpg|jpeg|gif)$/)) { @@ -143,7 +140,10 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } & }), }); } else { - addService(form.values); + setConfig({ + ...config, + services: [...config.services, form.values], + }); } setOpened(false); form.reset(); diff --git a/components/AppShelf/AppShelf.tsx b/components/AppShelf/AppShelf.tsx index fb0b23f7e..af5f85340 100644 --- a/components/AppShelf/AppShelf.tsx +++ b/components/AppShelf/AppShelf.tsx @@ -1,35 +1,18 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { motion } from 'framer-motion'; -import { - Text, - AspectRatio, - SimpleGrid, - Card, - useMantineTheme, - Image, - Group, - Space, -} from '@mantine/core'; +import { Text, AspectRatio, SimpleGrid, Card, Image, Group, Space } from '@mantine/core'; import { useConfig } from '../../tools/state'; import { serviceItem } from '../../tools/types'; import AddItemShelfItem from './AddAppShelfItem'; import { AppShelfItemWrapper } from './AppShelfItemWrapper'; import AppShelfMenu from './AppShelfMenu'; -const AppShelf = (props: any) => { - const { config, addService, removeService, setConfig } = useConfig(); - - /* A hook that is used to load the config from local storage. */ - useEffect(() => { - const localConfig = localStorage.getItem('config'); - if (localConfig) { - setConfig(JSON.parse(localConfig)); - } - }, []); +const AppShelf = () => { + const { config } = useConfig(); return ( - {config.services.map((service, i) => ( + {config.services.map((service) => ( ))} @@ -39,16 +22,14 @@ const AppShelf = (props: any) => { export function AppShelfItem(props: any) { const { service }: { service: serviceItem } = props; - const theme = useMantineTheme(); - const { removeService } = useConfig(); const [hovering, setHovering] = useState(false); return ( { + onHoverStart={() => { setHovering(true); }} - onHoverEnd={(e) => { + onHoverEnd={() => { setHovering(false); }} > @@ -79,7 +60,7 @@ export function AppShelfItem(props: any) { opacity: hovering ? 1 : 0, }} > - + diff --git a/components/AppShelf/AppShelfMenu.tsx b/components/AppShelf/AppShelfMenu.tsx index 95042ddf1..ae905dfbf 100644 --- a/components/AppShelf/AppShelfMenu.tsx +++ b/components/AppShelf/AppShelfMenu.tsx @@ -2,10 +2,12 @@ import { Menu, Modal, Text } from '@mantine/core'; import { showNotification } from '@mantine/notifications'; import { useState } from 'react'; import { Check, Edit, Trash } from 'tabler-icons-react'; +import { useConfig } from '../../tools/state'; import { AddAppShelfItemForm } from './AddAppShelfItem'; export default function AppShelfMenu(props: any) { - const { service, removeitem: removeItem } = props; + const { service } = props; + const { config, setConfig } = useConfig(); const [opened, setOpened] = useState(false); return ( <> @@ -40,7 +42,10 @@ export default function AppShelfMenu(props: any) { { - removeItem(service.name); + setConfig({ + ...config, + services: config.services.filter((s) => s.name !== service.name), + }); showNotification({ autoClose: 5000, title: ( diff --git a/components/Config/ConfigChanger.tsx b/components/Config/ConfigChanger.tsx new file mode 100644 index 000000000..5b8836803 --- /dev/null +++ b/components/Config/ConfigChanger.tsx @@ -0,0 +1,37 @@ +import { Center, Loader, Select, Tooltip } from '@mantine/core'; +import { setCookies } from 'cookies-next'; +import { useEffect, useState } from 'react'; +import { useConfig } from '../../tools/state'; + +export default function ConfigChanger() { + const { config, loadConfig, setConfig, getConfigs } = useConfig(); + const [configList, setConfigList] = useState([] as string[]); + useEffect(() => { + getConfigs().then((configs) => setConfigList(configs)); + // setConfig(initialConfig); + }, [config]); + // If configlist is empty, return a loading indicator + if (configList.length === 0) { + return ( +
+ + + +
+ ); + } + return ( + + ); +} diff --git a/components/Settings/SettingsMenu.tsx b/components/Settings/SettingsMenu.tsx index 47a41fee1..b8fed02eb 100644 --- a/components/Settings/SettingsMenu.tsx +++ b/components/Settings/SettingsMenu.tsx @@ -16,6 +16,7 @@ import { AlertCircle, Settings as SettingsIcon } from 'tabler-icons-react'; import { CURRENT_VERSION, REPO_URL } from '../../data/constants'; import { useConfig } from '../../tools/state'; import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch'; +import ConfigChanger from '../Config/ConfigChanger'; import SaveConfigComponent from '../Config/SaveConfig'; import ModuleEnabler from './ModuleEnabler'; @@ -28,7 +29,6 @@ function SettingsMenu(props: any) { { label: 'DuckDuckGo', value: 'https://duckduckgo.com/?q=' }, { label: 'Bing', value: 'https://bing.com/search?q=' }, ]; - return ( match.value === config.settings.searchUrl)?.value || 'Google' + matches.find((match) => match.value === config.settings.searchUrl)?.value ?? 'Google' } onChange={ // Set config.settings.searchUrl to the value of the selected item @@ -79,6 +79,7 @@ function SettingsMenu(props: any) { + { const nextColorScheme = value || (colorScheme === 'dark' ? 'light' : 'dark'); setColorScheme(nextColorScheme); - setCookies('mantine-color-scheme', nextColorScheme, { maxAge: 60 * 60 * 24 * 30 }); + setCookies('color-scheme', nextColorScheme, { maxAge: 60 * 60 * 24 * 30 }); }; return ( @@ -50,5 +50,5 @@ export default function App(props: AppProps & { colorScheme: ColorScheme }) { } App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => ({ - colorScheme: getCookie('mantine-color-scheme', ctx) || 'light', + colorScheme: getCookie('color-scheme', ctx) || 'light', }); diff --git a/pages/api/configs/[slug].ts b/pages/api/configs/[slug].ts new file mode 100644 index 000000000..dfa8ed8d0 --- /dev/null +++ b/pages/api/configs/[slug].ts @@ -0,0 +1,61 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import fs from 'fs'; +import path from 'path'; + +function Put(req: NextApiRequest, res: NextApiResponse) { + // Get the slug of the request + const { slug } = req.query as { slug: string }; + // Get the body of the request + const { body }: { body: string } = req; + if (!slug || !body) { + res.status(400).json({ + error: 'Wrong request', + }); + } + // Save the body in the /data/config folder with the slug as filename + + fs.writeFileSync( + path.join('data/configs', `${slug}.json`), + JSON.stringify(body, null, 2), + 'utf8' + ); + return res.status(200).json({ + message: 'Configuration saved with success', + }); +} + +function Get(req: NextApiRequest, res: NextApiResponse) { + // Get the slug of the request + const { slug } = req.query as { slug: string }; + if (!slug) { + return res.status(400).json({ + message: 'Wrong request', + }); + } + // Loop over all the files in the /data/configs directory + const files = fs.readdirSync('data/configs'); + // Strip the .json extension from the file name + const configs = files.map((file) => file.replace('.json', '')); + // If the target is not in the list of files, return an error + if (!configs.includes(slug)) { + return res.status(404).json({ + message: 'Target not found', + }); + } + // Return the content of the file + return res.status(200).json(fs.readFileSync(path.join('data/configs', `${slug}.json`), 'utf8')); +} + +export default async (req: NextApiRequest, res: NextApiResponse) => { + // Filter out if the reuqest is a Put or a GET + if (req.method === 'PUT') { + return Put(req, res); + } + if (req.method === 'GET') { + return Get(req, res); + } + return res.status(405).json({ + statusCode: 405, + message: 'Method not allowed', + }); +}; diff --git a/pages/api/configs/index.ts b/pages/api/configs/index.ts new file mode 100644 index 000000000..78aabc4e8 --- /dev/null +++ b/pages/api/configs/index.ts @@ -0,0 +1,28 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import fs from 'fs'; + +function Get(req: NextApiRequest, res: NextApiResponse) { + // Loop over all the files in the /data/configs directory + const files = fs.readdirSync('data/configs'); + // Strip the .json extension from the file name + const configs = files.map((file) => file.replace('.json', '')); + + return res.status(200).json(configs); +} + +export default async (req: NextApiRequest, res: NextApiResponse) => { + // Filter out if the reuqest is a POST or a GET + if (req.method === 'POST') { + return res.status(405).json({ + statusCode: 405, + message: 'Method not allowed', + }); + } + if (req.method === 'GET') { + return Get(req, res); + } + return res.status(405).json({ + statusCode: 405, + message: 'Method not allowed', + }); +}; diff --git a/pages/index.tsx b/pages/index.tsx index 7203f0f44..8ec813ffb 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,9 +1,57 @@ import { Group } from '@mantine/core'; +import { getCookie, setCookies } from 'cookies-next'; +import { GetServerSidePropsContext } from 'next'; +import path from 'path'; +import fs from 'fs'; +import { useEffect } from 'react'; import AppShelf from '../components/AppShelf/AppShelf'; import LoadConfigComponent from '../components/Config/LoadConfig'; import SearchBar from '../components/SearchBar/SearchBar'; +import { Config } from '../tools/types'; +import { useConfig } from '../tools/state'; -export default function HomePage() { +export async function getServerSideProps({ + req, + res, +}: GetServerSidePropsContext): Promise<{ props: { config: Config } }> { + let cookie = getCookie('config-name', { req, res }); + if (!cookie) { + setCookies('config-name', 'default', { req, res, maxAge: 60 * 60 * 24 * 30 }); + cookie = 'default'; + } + // Check if the config file exists + const configPath = path.join(process.cwd(), 'data/configs', `${cookie}.json`); + if (!fs.existsSync(configPath)) { + return { + props: { + config: { + name: cookie.toString(), + services: [], + settings: { + enabledModules: [], + searchBar: true, + searchUrl: 'https://www.google.com/search?q=', + }, + }, + }, + }; + } + + const config = fs.readFileSync(configPath, 'utf8'); + // Print loaded config + return { + props: { + config: JSON.parse(config), + }, + }; +} + +export default function HomePage(props: any) { + const { config: initialConfig }: { config: Config } = props; + const { config, loadConfig, setConfig, getConfigs } = useConfig(); + useEffect(() => { + setConfig(initialConfig); + }, [initialConfig]); return ( <> diff --git a/pages/tryconfig.tsx b/pages/tryconfig.tsx new file mode 100644 index 000000000..6fb38734c --- /dev/null +++ b/pages/tryconfig.tsx @@ -0,0 +1,101 @@ +import { getCookie, setCookies } from 'cookies-next'; +import { GetServerSidePropsContext } from 'next/types'; +import fs from 'fs'; +import path from 'path'; +import { Button, JsonInput, Select, Space } from '@mantine/core'; +import { useEffect, useState } from 'react'; +import { Config } from '../tools/types'; +import { useConfig } from '../tools/state'; + +export async function getServerSideProps({ + req, + res, +}: GetServerSidePropsContext): Promise<{ props: { config: Config } }> { + let cookie = getCookie('config-name', { req, res }); + if (!cookie) { + setCookies('config-name', 'default', { req, res, maxAge: 60 * 60 * 24 * 30 }); + cookie = 'default'; + } + // Check if the config file exists + const configPath = path.join(process.cwd(), 'data/configs', `${cookie}.json`); + if (!fs.existsSync(configPath)) { + return { + props: { + config: { + name: cookie.toString(), + services: [], + settings: { + enabledModules: [], + searchBar: true, + searchUrl: 'https://www.google.com/search?q=', + }, + }, + }, + }; + } + + const config = fs.readFileSync(configPath, 'utf8'); + // Print loaded config + return { + props: { + config: JSON.parse(config), + }, + }; +} + +export default function TryConfig(props: any) { + const { config: initialConfig }: { config: Config } = props; + const { config, loadConfig, setConfig, getConfigs } = useConfig(); + const [value, setValue] = useState(JSON.stringify(config, null, 2)); + const [configList, setConfigList] = useState([] as string[]); + useEffect(() => { + setValue(JSON.stringify(initialConfig, null, 2)); + setConfig(initialConfig); + }, [initialConfig]); + useEffect(() => { + setValue(JSON.stringify(config, null, 2)); + // setConfig(initialConfig); + }, [config]); + + return ( +
+

Try Config

+

+ This page is a demo of the config API. +

+

+ The config API is a way to store configuration data in a JSON file. +

+

+ Cookie loaded was {initialConfig.name} +

+ + + + +