diff --git a/.eslintrc.js b/.eslintrc.js index b682aa9dc..abde8f7cb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,6 +20,7 @@ module.exports = { }, rules: { 'react/react-in-jsx-scope': 'off', + 'react/no-children-prop': 'off', "unused-imports/no-unused-imports": "warn", "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-imports": "off", diff --git a/README.md b/README.md index 85ca62ba8..221858ee8 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ - [๐Ÿ“Š Modules](#-modules) - [๐Ÿ” Search Bar](#-search-bar) - [๐Ÿ’– Contributing](#-contributing) + - [๐Ÿ Request Icons](#-request-icons) @@ -190,5 +191,11 @@ The Search Bar will open any Search Query after the Query URL you've specified i **Please read our [Contribution Guidelines](/CONTRIBUTING.md)** All contributions are highly appreciated. + +**[โคด๏ธ Back to Top](#-table-of-contents)** + +## ๐Ÿ Request Icons + +The icons used in Homarr are automatically requested from the [dashboard-icons](https://github.com/walkxhub/dashboard-icons) repo. You can make a icon request by creating an [issue](https://github.com/walkxhub/dashboard-icons/issues/new/choose). **[โคด๏ธ Back to Top](#-table-of-contents)** diff --git a/src/components/AppShelf/AppShelf.tsx b/src/components/AppShelf/AppShelf.tsx index 761320294..41ac8baf5 100644 --- a/src/components/AppShelf/AppShelf.tsx +++ b/src/components/AppShelf/AppShelf.tsx @@ -52,8 +52,8 @@ export function AppShelfItem(props: any) { setOpened(false)} title="Modify a service" @@ -28,7 +29,16 @@ export default function AppShelfMenu(props: any) { message="Save service" /> - + Settings - } - title="Update available" - radius="lg" - hidden={current === latest} - > - Version {latest} is available. Current: {current} - Search engine { - // Fetch Data here when component first mounted - fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => { - res.json().then((data) => { - setLatestVersion(data.tag_name); - if (data.tag_name !== CURRENT_VERSION) { - setUpdate(true); - } - }); - }); - }, []); return ( <> setOpened(false)} > - + setOpened(true)} > - - - + diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index e93a4ff7f..b944eb5f5 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { createStyles, Anchor, @@ -6,9 +6,11 @@ import { Group, ActionIcon, Footer as FooterComponent, + Alert, + useMantineTheme, } from '@mantine/core'; -import { BrandGithub } from 'tabler-icons-react'; -import { CURRENT_VERSION } from '../../../data/constants'; +import { AlertCircle, BrandGithub } from 'tabler-icons-react'; +import { CURRENT_VERSION, REPO_URL } from '../../../data/constants'; const useStyles = createStyles((theme) => ({ footer: { @@ -41,6 +43,8 @@ interface FooterCenteredProps { } export function Footer({ links }: FooterCenteredProps) { + const [update, setUpdate] = useState(false); + const theme = useMantineTheme(); const { classes } = useStyles(); const items = links.map((link) => ( @@ -55,42 +59,88 @@ export function Footer({ links }: FooterCenteredProps) { )); + const [latestVersion, setLatestVersion] = useState(CURRENT_VERSION); + const [isOpen, setOpen] = useState(true); + useEffect(() => { + // Fetch Data here when component first mounted + fetch(`https://api.github.com/repos/${REPO_URL}/releases/latest`).then((res) => { + res.json().then((data) => { + setLatestVersion(data.tag_name); + if (data.tag_name !== CURRENT_VERSION) { + setUpdate(true); + } + }); + }); + }, []); + return ( - - - component="a" href="https://github.com/ajnart/homarr" size="lg"> - - + + + setOpen(false)} + icon={} + title={`Updated version: ${latestVersion} is available. Current version: ${CURRENT_VERSION}`} + withCloseButton + radius="lg" + hidden={CURRENT_VERSION === latestVersion || !isOpen} + variant="outline" + styles={{ + root: { + backgroundColor: + theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1], + }, + + closeButton: { + marginLeft: '5px', + }, + }} + children={undefined} + /> + + + + component="a" href="https://github.com/ajnart/homarr" size="lg"> + + + + {CURRENT_VERSION} + + - {CURRENT_VERSION} + Made with โค๏ธ by @ + + ajnart + - - Made with โค๏ธ by @ - - ajnart - - ); diff --git a/src/components/modules/date/DateModule.tsx b/src/components/modules/date/DateModule.tsx index d53c160ee..7b31f859e 100644 --- a/src/components/modules/date/DateModule.tsx +++ b/src/components/modules/date/DateModule.tsx @@ -2,6 +2,7 @@ import { Group, Text, Title } from '@mantine/core'; import dayjs from 'dayjs'; import { useEffect, useState } from 'react'; import { Clock } from 'tabler-icons-react'; +import { useConfig } from '../../../tools/state'; import { IModule } from '../modules'; export const DateModule: IModule = { @@ -9,33 +10,39 @@ export const DateModule: IModule = { description: 'Show the current time and date in a card', icon: Clock, component: DateComponent, + options: { + full: { + name: 'Display full time (24-hour)', + value: true, + }, + }, }; export default function DateComponent(props: any) { const [date, setDate] = useState(new Date()); + const { config } = useConfig(); const hours = date.getHours(); const minutes = date.getMinutes(); - + const fullSetting = config.settings[`${DateModule.title}.full`]; // Change date on minute change // Note: Using 10 000ms instead of 1000ms to chill a little :) useEffect(() => { setInterval(() => { setDate(new Date()); - }, 10000); + }, 1000 * 60); }, []); + const timeString = `${hours < 10 ? `0${hours}` : hours}:${ + minutes < 10 ? `0${minutes}` : minutes + }`; + const halfTimeString = `${hours < 10 ? `${hours % 12}` : hours % 12}:${ + minutes < 10 ? `0${minutes}` : minutes + } ${hours < 12 ? 'AM' : 'PM'}`; + const finalTimeString = fullSetting ? timeString : halfTimeString; return ( - - - {hours < 10 ? `0${hours}` : hours}:{minutes < 10 ? `0${minutes}` : minutes} - - - { - // Use dayjs to format the date - // https://day.js.org/en/getting-started/installation/ - dayjs(date).format('dddd, MMMM D') - } - + + {finalTimeString} + {dayjs(date).format('dddd, MMMM D')} ); } diff --git a/src/components/modules/moduleWrapper.tsx b/src/components/modules/moduleWrapper.tsx index 64a648eef..4419ecdc8 100644 --- a/src/components/modules/moduleWrapper.tsx +++ b/src/components/modules/moduleWrapper.tsx @@ -1,19 +1,82 @@ -import { Card, useMantineTheme } from '@mantine/core'; +import { Card, Menu, Switch, useMantineTheme } from '@mantine/core'; import { useConfig } from '../../tools/state'; import { IModule } from './modules'; export function ModuleWrapper(props: any) { const { module }: { module: IModule } = props; - const { config } = useConfig(); + const { config, setConfig } = useConfig(); const enabledModules = config.settings.enabledModules ?? []; // Remove 'Module' from enabled modules titles const isShown = enabledModules.includes(module.title); const theme = useMantineTheme(); + const items: JSX.Element[] = []; + if (module.options) { + const keys = Object.keys(module.options); + const values = Object.values(module.options); + // Get the value and the name of the option + const types = values.map((v) => typeof v.value); + // Loop over all the types with a for each loop + types.forEach((type, index) => { + const optionName = `${module.title}.${keys[index]}`; + // TODO: Add support for other types + if (type === 'boolean') { + items.push( + { + setConfig({ + ...config, + settings: { + ...config.settings, + enabledModules: [...config.settings.enabledModules], + [optionName]: e.currentTarget.checked, + }, + }); + }} + label={values[index].name} + /> + ); + } + }); + } + // Sussy baka if (!isShown) { return null; } return ( ); diff --git a/src/components/modules/modules.tsx b/src/components/modules/modules.tsx index 01fc8223d..c3c3ef1dd 100644 --- a/src/components/modules/modules.tsx +++ b/src/components/modules/modules.tsx @@ -6,6 +6,15 @@ export interface IModule { title: string; description: string; icon: React.ReactNode; - component: (args: any) => JSX.Element | null; - props?: any; + component: React.ComponentType; + options?: Option; +} + +interface Option { + [x: string]: OptionValues; +} + +interface OptionValues { + name: string; + value: boolean; }