Compare commits

...

53 Commits

Author SHA1 Message Date
Thomas Camlong
2586733a98 v0.4.0
Add Weather and Ping module
2022-05-18 23:22:14 +02:00
ajnart
7bc779b296 ⚰️ Remove dead code
Used to test the weather module
2022-05-18 23:13:32 +02:00
ajnart
6064dcb6a6 💄 Footer styling 2022-05-18 23:12:52 +02:00
ajnart
7c7b0cc970 💫 Add animations to the AppShelf 2022-05-18 23:12:34 +02:00
ajnart
c182397dd9 💫 Add animations to the PingModule 2022-05-18 23:11:58 +02:00
ajnart
dc5ee3bdf3 Add animations to the AppShelf 2022-05-18 22:51:12 +02:00
ajnart
c8e1295a4b Improve date module am/pm 2022-05-18 22:50:53 +02:00
ajnart
331c55240b Added Freedom units setting 2022-05-18 22:50:33 +02:00
Thomas Camlong
65037f9b56 Add Weather module (beta)
Shows the current weather !
2022-05-18 22:17:58 +02:00
Bjorn L
39853d79ce 🔧 Change versions to v0.4.0 2022-05-18 22:15:03 +02:00
Bjorn L
8530550347 🔧 Change versions to v0.4.0 2022-05-18 22:14:27 +02:00
Thomas Camlong
ba8e9ef63c Merge branch 'dev' into weather-module 2022-05-18 22:14:01 +02:00
ajnart
119f2d7e51 Add a proceudally generated options manager
This allows for options in settings generated based on their name in module config. Very important change 🧙
2022-05-18 22:11:37 +02:00
ajnart
b0be26300e 💄 Update AppShell menu and item styling
Co-authored-by: Bjorn L. <walkxnl@gmail.com>
2022-05-18 22:10:31 +02:00
ajnart
0400188ea7 🚚 Move the update indicator to the Footer
Co-authored-by: Bjorn L. <walkxnl@gmail.com>
2022-05-18 22:09:13 +02:00
ajnart
879581224a 🔥 Remove update indicator from settings
Co-authored-by: Bjorn L. <walkxnl@gmail.com>
2022-05-18 22:08:09 +02:00
ajnart
7e5602c881 🚨 Update eslint config
Co-authored-by: Bjorn L. <walkxnl@gmail.com>
2022-05-18 22:07:28 +02:00
Bjorn L
4870ea3e40 📝 Adds Docs for the Weather Module 2022-05-18 16:58:06 +02:00
Bjorn L
61c55acd50 📝 Adds Request Icons section 2022-05-18 16:55:48 +02:00
Thomas Camlong
c45421d27e Merge branch 'dev' into weather-module 2022-05-18 10:24:16 +02:00
Thomas "ajnart" Camlong
b396d2604f 🚑 Critical hotfix : Compilation failed 2022-05-18 10:23:18 +02:00
Thomas "ajnart" Camlong
28b6dcd1db 📦 Update deps 2022-05-18 10:10:42 +02:00
Thomas "ajnart" Camlong
1dd74ea7da 🐛 Try to fix module compilation 2022-05-17 22:59:02 +02:00
Thomas "ajnart" Camlong
64923b03d9 🎨 Fix architecture for CI 2022-05-17 22:59:02 +02:00
Thomas "ajnart" Camlong
2ba9d517a8 Improve weather module 2022-05-17 22:59:02 +02:00
Aj - Thomas
471a9f7407 Update page title 2022-05-17 22:59:02 +02:00
Aj - Thomas
bdf871b476 💄 � Update weather module styling 2022-05-17 22:59:02 +02:00
Aj - Thomas
ab860eeea1 � Weather module improvements 2022-05-17 22:59:02 +02:00
Thomas "ajnart" Camlong
50d760f3b8 Prepare for v0.3.2 2022-05-17 21:24:10 +02:00
Thomas "ajnart" Camlong
73d06e15fb Update tests for storybook 2022-05-17 21:04:19 +02:00
Thomas "ajnart" Camlong
49d57024b9 Advancement on the weather widget 2022-05-17 21:04:19 +02:00
Thomas "ajnart" Camlong
31deb5010f 💄 Improve styling of modules 2022-05-17 21:04:18 +02:00
Thomas "ajnart" Camlong
e86eb7798f 🚧 Set up the structure for the weather module 2022-05-17 21:04:16 +02:00
Thomas Camlong
2896423766 Add ping service module
 Add ping service module resolves #78
2022-05-17 20:59:44 +02:00
Thomas "ajnart" Camlong
696d0c582d 🐛 Clear the search input on search
Resolves #125
2022-05-17 20:58:55 +02:00
ajnart
e94cae620a Rever b7e8c51b29
Does not work. Apparently
2022-05-17 04:19:59 +02:00
ajnart
c9c6f2b0c9 Add ping service module
Resolves #78
2022-05-17 04:02:14 +02:00
ajnart
b8fe799ac6 ⚰️ Remove dead code for the settings
I turned the settings into a module in 4cb8539143
2022-05-17 02:07:38 +02:00
ajnart
4cb8539143 Make the search bar a module
Resolves #118
2022-05-17 02:04:44 +02:00
ajnart
16b86870c4 🏗️ Fix small bug in code arch, forgot the key 2022-05-17 02:03:52 +02:00
ajnart
d4ce2a3ed6 🏷️ Update types for the SearchBar 2022-05-17 01:52:43 +02:00
ajnart
a474f3e4ee 🥅 Add 404 to catch errors
Reduce the ammount of visible errors by adding a 404 page.
2022-05-17 01:44:26 +02:00
ajnart
9a49fbb747 💄 Update AppShelf UI 2022-05-17 01:43:40 +02:00
ajnart
e3d47d78e0 🐛 Add a delay before opening search results
Resolves #115
2022-05-17 01:23:19 +02:00
ajnart
d62189f086 💄 Remove version from logo and add it in footer
resolves #116
2022-05-17 01:01:26 +02:00
ajnart
bb1b3d7d9a Merge branch 'dev' of github.com:/ajnart/homarr into dev 2022-05-17 00:55:44 +02:00
ajnart
13aeeefb22 🐛 Fix AddAppShelfItem image fit not properly set
Resolves #117
2022-05-17 00:55:24 +02:00
ajnart
8cdc9c3e29 🎨 Use user prefered theme 2022-05-17 00:42:27 +02:00
ajnart
3e31a4d38e 💄 Better style events in the calendar 2022-05-17 00:42:27 +02:00
ajnart
0cb3db6b89 📦 Upgrade package 2022-05-17 00:42:27 +02:00
ajnart
b7e8c51b29 🎨 Use user prefered theme 2022-05-17 00:19:41 +02:00
ajnart
e60db9f57a 💄 Better style events in the calendar 2022-05-17 00:19:24 +02:00
ajnart
2c707e86aa 📦 Upgrade package 2022-05-17 00:18:22 +02:00
35 changed files with 2002 additions and 1480 deletions

View File

@@ -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",

View File

@@ -3,6 +3,7 @@ module.exports = {
addons: [
'storybook-dark-mode',
'@storybook/addon-links',
'storybook-addon-mock/register',
'@storybook/addon-essentials',
{
name: 'storybook-addon-turbo-build',

View File

@@ -39,6 +39,7 @@
- [📊 Modules](#-modules)
- [🔍 Search Bar](#-search-bar)
- [💖 Contributing](#-contributing)
- [🍏 Request Icons](#-request-icons)
<!-- Getting Started -->
@@ -168,10 +169,13 @@ Icons are requested in the following way: <br>
Modules are blocks shown on the sides of the Homarr dashboard that display information. They can be enabled in settings.
**Clock Module**
The clock module will display your current time and date.
The Clock Module will display your current time and date.
**Calendar Module**
The Calendar module uses [integrations](#--integrations-1) to display new content.
The Calendar Module uses [integrations](#--integrations-1) to display new content.
**Weather Module**
The Weather Module uses your devices location to display the current, highest, and lowest temperature.
**[⤴️ Back to Top](#-table-of-contents)**
@@ -187,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)**

View File

@@ -1,2 +1,2 @@
export const REPO_URL = 'ajnart/homarr';
export const CURRENT_VERSION = 'v0.3.1';
export const CURRENT_VERSION = 'v0.4.0';

View File

@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "0.3.0",
"version": "0.4.0",
"private": "false",
"description": "Homarr - A homepage for your server.",
"repository": {
@@ -79,9 +79,13 @@
"eslint-plugin-unused-imports": "^2.0.0",
"jest": "^27.5.1",
"prettier": "^2.6.2",
"storybook-addon-mock": "^2.3.2",
"storybook-addon-turbo-build": "^1.1.0",
"storybook-dark-mode": "^1.0.9",
"ts-jest": "^27.1.4",
"typescript": "4.6.3"
},
"resolutions": {
"@types/react": "17.0.30"
}
}

View File

@@ -154,7 +154,14 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
return (
<>
<Center>
<Image height={120} width={120} src={form.values.icon} alt="Placeholder" withPlaceholder />
<Image
height={120}
width={120}
fit="contain"
src={form.values.icon}
alt="Placeholder"
withPlaceholder
/>
</Center>
<form
onSubmit={form.onSubmit(() => {

View File

@@ -1,17 +1,28 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { Text, AspectRatio, Card, Image, useMantineTheme, Center, Grid } from '@mantine/core';
import { Text, AspectRatio, Card, Image, Center, Grid, createStyles } from '@mantine/core';
import { useConfig } from '../../tools/state';
import { serviceItem } from '../../tools/types';
import AppShelfMenu from './AppShelfMenu';
import PingComponent from '../modules/ping/PingModule';
const useStyles = createStyles((theme) => ({
item: {
transition: 'box-shadow 150ms ease, transform 100ms ease',
'&:hover': {
boxShadow: `${theme.shadows.md} !important`,
transform: 'scale(1.05)',
},
},
}));
const AppShelf = (props: any) => {
const { config } = useConfig();
return (
<Grid gutter="xl" align="center">
{config.services.map((service) => (
<Grid.Col span={6} xl={2} xs={4} sm={3} md={3}>
<Grid.Col key={service.name} span={6} xl={2} xs={4} sm={3} md={3}>
<AppShelfItem key={service.name} service={service} />
</Grid.Col>
))}
@@ -22,9 +33,14 @@ const AppShelf = (props: any) => {
export function AppShelfItem(props: any) {
const { service }: { service: serviceItem } = props;
const [hovering, setHovering] = useState(false);
const theme = useMantineTheme();
const { classes, theme } = useStyles();
return (
<motion.div
animate={{
scale: [0.9, 1.06, 1],
rotate: [0, 5, 0],
}}
transition={{ duration: 0.6, type: 'spring', damping: 10, mass: 0.75, stiffness: 100 }}
key={service.name}
onHoverStart={() => {
setHovering(true);
@@ -33,7 +49,7 @@ export function AppShelfItem(props: any) {
setHovering(false);
}}
>
<Card withBorder radius="lg" shadow="md">
<Card withBorder radius="lg" shadow="md" className={classes.item}>
<Card.Section>
<Text mt="sm" align="center" lineClamp={1} weight={550}>
{service.name}
@@ -41,8 +57,8 @@ export function AppShelfItem(props: any) {
<motion.div
style={{
position: 'absolute',
top: 5,
right: 5,
top: 15,
right: 15,
alignSelf: 'flex-end',
}}
animate={{
@@ -79,6 +95,7 @@ export function AppShelfItem(props: any) {
/>
</motion.i>
</AspectRatio>
<PingComponent url={service.url} />
</Card.Section>
</Center>
</Card>

View File

@@ -1,4 +1,4 @@
import { Menu, Modal, Text } from '@mantine/core';
import { Menu, Modal, Text, useMantineTheme } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { useState } from 'react';
import { Check, Edit, Trash } from 'tabler-icons-react';
@@ -8,12 +8,13 @@ import { AddAppShelfItemForm } from './AddAppShelfItem';
export default function AppShelfMenu(props: any) {
const { service } = props;
const { config, setConfig } = useConfig();
const theme = useMantineTheme();
const [opened, setOpened] = useState(false);
return (
<>
<Modal
size="xl"
radius="lg"
radius="md"
opened={props.opened || opened}
onClose={() => setOpened(false)}
title="Modify a service"
@@ -28,7 +29,16 @@ export default function AppShelfMenu(props: any) {
message="Save service"
/>
</Modal>
<Menu position="right">
<Menu
position="right"
radius="md"
styles={{
body: {
backgroundColor:
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
},
}}
>
<Menu.Label>Settings</Menu.Label>
<Menu.Item
color="primary"

View File

@@ -2,19 +2,15 @@ import {
ActionIcon,
Group,
Modal,
Switch,
Title,
Text,
Tooltip,
SegmentedControl,
Indicator,
Alert,
TextInput,
} from '@mantine/core';
import { useColorScheme } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { AlertCircle, Settings as SettingsIcon } from 'tabler-icons-react';
import { CURRENT_VERSION, REPO_URL } from '../../../data/constants';
import { useState } from 'react';
import { Settings as SettingsIcon } from 'tabler-icons-react';
import { useConfig } from '../../tools/state';
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
import ConfigChanger from '../Config/ConfigChanger';
@@ -40,14 +36,6 @@ function SettingsMenu(props: any) {
return (
<Group direction="column" grow>
<Alert
icon={<AlertCircle size={16} />}
title="Update available"
radius="lg"
hidden={current === latest}
>
Version {latest} is available. Current: {current}
</Alert>
<Group grow direction="column" spacing={0}>
<Text>Search engine</Text>
<SegmentedControl
@@ -90,22 +78,6 @@ function SettingsMenu(props: any) {
/>
)}
</Group>
<Group direction="column">
<Switch
size="md"
onChange={(e) =>
setConfig({
...config,
settings: {
...config.settings,
searchBar: e.currentTarget.checked,
},
})
}
checked={config.settings.searchBar}
label="Enable search bar"
/>
</Group>
<ModuleEnabler />
<ColorSchemeSwitch />
<ConfigChanger />
@@ -125,20 +97,7 @@ function SettingsMenu(props: any) {
}
export function SettingsMenuButton(props: any) {
const [update, setUpdate] = useState(false);
const [opened, setOpened] = useState(false);
const [latestVersion, setLatestVersion] = useState(CURRENT_VERSION);
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 (
<>
<Modal
@@ -148,7 +107,7 @@ export function SettingsMenuButton(props: any) {
opened={props.opened || opened}
onClose={() => setOpened(false)}
>
<SettingsMenu current={CURRENT_VERSION} latest={latestVersion} />
<SettingsMenu />
</Modal>
<ActionIcon
variant="default"
@@ -159,14 +118,7 @@ export function SettingsMenuButton(props: any) {
onClick={() => setOpened(true)}
>
<Tooltip label="Settings">
<Indicator
size={12}
disabled={CURRENT_VERSION === latestVersion}
offset={-3}
position="top-end"
>
<SettingsIcon />
</Indicator>
<SettingsIcon />
</Tooltip>
</ActionIcon>
</>

View File

@@ -1,7 +1,6 @@
import { Aside as MantineAside, Group } from '@mantine/core';
import { DateModule } from '../modules';
import { CalendarModule } from '../modules/calendar/CalendarModule';
import ModuleWrapper from '../modules/moduleWrapper';
import { WeatherModule, DateModule, CalendarModule } from '../modules';
import { ModuleWrapper } from '../modules/moduleWrapper';
export default function Aside(props: any) {
return (
@@ -18,6 +17,7 @@ export default function Aside(props: any) {
<Group mt="sm" grow direction="column">
<ModuleWrapper module={CalendarModule} />
<ModuleWrapper module={DateModule} />
<ModuleWrapper module={WeatherModule} />
</Group>
</MantineAside>
);

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import {
createStyles,
Anchor,
@@ -6,8 +6,11 @@ import {
Group,
ActionIcon,
Footer as FooterComponent,
Alert,
useMantineTheme,
} from '@mantine/core';
import { BrandGithub } from 'tabler-icons-react';
import { AlertCircle, BrandGithub } from 'tabler-icons-react';
import { CURRENT_VERSION, REPO_URL } from '../../../data/constants';
const useStyles = createStyles((theme) => ({
footer: {
@@ -40,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) => (
<Anchor<'a'>
@@ -54,27 +59,87 @@ export function Footer({ links }: FooterCenteredProps) {
</Anchor>
));
return (
<FooterComponent p={5} height="auto" style={{ border: 'none', position: 'fixed', bottom: 0, right: 0 }}>
<Group position="right" mr="xs" mb="xs">
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
<BrandGithub size={18} />
</ActionIcon>
<Text
style={{
fontSize: '0.90rem',
textAlign: 'center',
color: '#a0aec0',
}}
>
Made with by @
<Anchor
href="https://github.com/ajnart"
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
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 (
<FooterComponent
p={5}
height="auto"
style={{
background: 'none',
border: 'none',
clear: 'both',
position: 'fixed',
bottom: '0',
left: '0',
}}
>
<Group position="apart" direction="row" style={{ alignItems: 'end' }} mr="xs" mb="xs">
<Group position="left">
<Alert
// onClick open latest release page
onClose={() => setOpen(false)}
icon={<AlertCircle size={16} />}
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] : 'white',
},
closeButton: {
marginLeft: '5px',
},
}}
children={undefined}
/>
</Group>
<Group position="right">
<Group spacing={0}>
<ActionIcon<'a'> component="a" href="https://github.com/ajnart/homarr" size="lg">
<BrandGithub size={18} />
</ActionIcon>
<Text
style={{
position: 'relative',
fontSize: '0.90rem',
color: 'gray',
}}
>
{CURRENT_VERSION}
</Text>
</Group>
<Text
style={{
fontSize: '0.90rem',
textAlign: 'center',
color: '#a0aec0',
}}
>
ajnart
</Anchor>
</Text>
Made with by @
<Anchor
href="https://github.com/ajnart"
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
>
ajnart
</Anchor>
</Text>
</Group>
</Group>
</FooterComponent>
);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { createStyles, Header as Head, Group, Box } from '@mantine/core';
import { Logo } from './Logo';
import SearchBar from '../SearchBar/SearchBar';
import SearchBar from '../modules/search/SearchModule';
import { AddItemShelfButton } from '../AppShelf/AddAppShelfItem';
import { SettingsMenuButton } from '../Settings/SettingsMenu';

View File

@@ -10,11 +10,7 @@ const useStyles = createStyles((theme) => ({
export default function Layout({ children, style }: any) {
const { classes, cx } = useStyles();
return (
<AppShell
aside={<Aside />}
header={<Header />}
footer={<Footer links={[]} />}
>
<AppShell aside={<Aside />} header={<Header />} footer={<Footer links={[]} />}>
<main
className={cx(classes.main)}
style={{

View File

@@ -1,16 +1,14 @@
import { Group, Image, Text } from '@mantine/core';
import * as React from 'react';
import { CURRENT_VERSION } from '../../../data/constants';
export function Logo({ style }: any) {
return (
<Group>
<Group spacing="xs">
<Image
width={50}
src="/imgs/logo.png"
style={{
position: 'relative',
left: 15,
}}
/>
<Text
@@ -21,20 +19,6 @@ export function Logo({ style }: any) {
>
Homarr
</Text>
<Text
style={{
position: 'relative',
left: -14,
bottom: -2,
color: 'gray',
fontStyle: 'inherit',
fontSize: 'inherit',
alignSelf: 'center',
alignContent: 'center',
}}
>
{CURRENT_VERSION}
</Text>
</Group>
);
}

View File

@@ -1,6 +1,6 @@
import { Group, Navbar as MantineNavbar } from '@mantine/core';
import { DateModule } from '../modules/date/DateModule';
import ModuleWrapper from '../modules/moduleWrapper';
import { WeatherModule, DateModule } from '../modules';
import { ModuleWrapper } from '../modules/moduleWrapper';
export default function Navbar() {
return (
@@ -16,6 +16,8 @@ export default function Navbar() {
>
<Group mt="sm" direction="column" align="center">
<ModuleWrapper module={DateModule} />
<ModuleWrapper module={WeatherModule} />
<ModuleWrapper module={WeatherModule} />
</Group>
</MantineNavbar>
);

View File

@@ -1,5 +1,5 @@
/* eslint-disable react/no-children-prop */
import { Popover, Box, ScrollArea, Divider, Indicator } from '@mantine/core';
import { Popover, Box, ScrollArea, Divider, Indicator, useMantineTheme } from '@mantine/core';
import React, { useEffect, useState } from 'react';
import { Calendar } from '@mantine/dates';
import { showNotification } from '@mantine/notifications';
@@ -93,6 +93,7 @@ function DayComponent(props: any) {
radarrmedias,
}: { renderdate: Date; sonarrmedias: []; radarrmedias: [] } = props;
const [opened, setOpened] = useState(false);
const theme = useMantineTheme();
const day = renderdate.getDate();
// Itterate over the medias and filter the ones that are on the same day
@@ -126,8 +127,7 @@ function DayComponent(props: any) {
width={700}
onClose={() => setOpened(false)}
opened={opened}
// TODO: Fix this !! WTF ?
target={` ${day}`}
target={day}
>
<ScrollArea style={{ height: 400 }}>
{sonarrFiltered.map((media: any, index: number) => (

View File

@@ -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,36 @@ 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 isFullTime =
config.settings[`${DateModule.title}.full`] === undefined
? true
: config.settings[`${DateModule.title}.full`];
const formatString = isFullTime ? 'HH:mm' : 'h:mm a';
// Change date on minute change
// Note: Using 10 000ms instead of 1000ms to chill a little :)
useEffect(() => {
setInterval(() => {
setDate(new Date());
}, 10000);
}, 1000 * 60);
}, []);
return (
<Group p="sm" direction="column">
<Title>
{hours < 10 ? `0${hours}` : hours}:{minutes < 10 ? `0${minutes}` : minutes}
</Title>
<Text size="xl">
{
// Use dayjs to format the date
// https://day.js.org/en/getting-started/installation/
dayjs(date).format('dddd, MMMM D')
}
</Text>
<Title>{dayjs(date).format(formatString)}</Title>
<Text size="xl">{dayjs(date).format('dddd, MMMM D')}</Text>
</Group>
);
}

View File

@@ -1,2 +1,5 @@
export * from './date';
export * from './calendar';
export * from './search';
export * from './ping';
export * from './weather';

View File

@@ -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 default function ModuleWrapper(props: any) {
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(
<Switch
defaultChecked={
// Set default checked to the value of the option if it exists
config.settings[optionName] ??
(module.options && module.options[keys[index]].value) ??
false
}
defaultValue={config.settings[optionName] ?? false}
key={keys[index]}
onClick={(e) => {
setConfig({
...config,
settings: {
...config.settings,
enabledModules: [...config.settings.enabledModules],
[optionName]: e.currentTarget.checked,
},
});
}}
label={values[index].name}
/>
);
}
});
}
// Sussy baka
if (!isShown) {
return null;
}
return (
<Card hidden={!isShown} mx="sm" withBorder radius="lg" shadow="sm">
{module.options && (
<Menu
size="md"
shadow="xl"
closeOnItemClick={false}
radius="md"
position="left"
styles={{
root: {
position: 'absolute',
top: 15,
right: 15,
},
body: {
backgroundColor:
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
},
}}
>
<Menu.Label>Settings</Menu.Label>
{items.map((item) => (
<Menu.Item key={item.key}>{item}</Menu.Item>
))}
</Menu>
)}
<module.component />
</Card>
);

View File

@@ -7,5 +7,14 @@ export interface IModule {
description: string;
icon: React.ReactNode;
component: React.ComponentType;
props?: any;
options?: Option;
}
interface Option {
[x: string]: OptionValues;
}
interface OptionValues {
name: string;
value: boolean;
}

View File

@@ -0,0 +1,15 @@
import { serviceItem } from '../../../tools/types';
import PingComponent from './PingModule';
export default {
title: 'Modules/Search bar',
};
const service: serviceItem = {
type: 'Other',
name: 'YouTube',
icon: 'https://cdn.jsdelivr.net/gh/walkxhub/dashboard-icons/png/youtube.png',
url: 'https://youtube.com/',
};
export const Default = (args: any) => <PingComponent service={service} />;

View File

@@ -0,0 +1,59 @@
import { Indicator, Tooltip } from '@mantine/core';
import axios from 'axios';
import { motion } from 'framer-motion';
import { useEffect, useState } from 'react';
import { Plug } from 'tabler-icons-react';
import { useConfig } from '../../../tools/state';
import { IModule } from '../modules';
export const PingModule: IModule = {
title: 'Ping Services',
description: 'Pings your services and shows their status as an indicator',
icon: Plug,
component: PingComponent,
};
export default function PingComponent(props: any) {
type State = 'loading' | 'down' | 'online';
const { config } = useConfig();
const { url }: { url: string } = props;
const [isOnline, setOnline] = useState<State>('loading');
useEffect(() => {
if (!config.settings.enabledModules.includes('Ping Services')) {
return;
}
axios
.get('/api/modules/ping', { params: { url } })
.then(() => {
setOnline('online');
})
.catch(() => {
setOnline('down');
});
}, []);
if (!config.settings.enabledModules.includes('Ping Services')) {
return null;
}
return (
<Tooltip
radius="lg"
style={{ position: 'absolute', bottom: 20, right: 20 }}
label={isOnline === 'loading' ? 'Loading...' : isOnline === 'online' ? 'Online' : 'Offline'}
>
<motion.div
animate={{
scale: isOnline === 'online' ? [1, 0.8, 1] : 1,
}}
transition={{ repeat: Infinity, duration: 2.5, ease: 'easeInOut' }}
>
<Indicator
size={13}
color={isOnline === 'online' ? 'green' : isOnline === 'down' ? 'red' : 'yellow'}
>
{null}
</Indicator>
</motion.div>
</Tooltip>
);
}

View File

@@ -0,0 +1 @@
export { PingModule } from './PingModule';

View File

@@ -1,4 +1,4 @@
import SearchBar from './SearchBar';
import SearchBar from './SearchModule';
export default {
title: 'Search bar',

View File

@@ -1,8 +1,9 @@
import { TextInput, Kbd, createStyles, useMantineTheme, Text, Popover } from '@mantine/core';
import { TextInput, Kbd, createStyles, Text, Popover } from '@mantine/core';
import { useForm, useHotkeys } from '@mantine/hooks';
import { useRef, useState } from 'react';
import { Search, BrandYoutube, Download } from 'tabler-icons-react';
import { useConfig } from '../../tools/state';
import { useConfig } from '../../../tools/state';
import { IModule } from '../modules';
const useStyles = createStyles((theme) => ({
hide: {
@@ -14,16 +15,22 @@ const useStyles = createStyles((theme) => ({
},
}));
export const SearchModule: IModule = {
title: 'Search Bar',
description: 'Show the current time and date in a card',
icon: Search,
component: SearchBar,
};
export default function SearchBar(props: any) {
const { config, setConfig } = useConfig();
const [opened, setOpened] = useState(false);
const [icon, setIcon] = useState(<Search />);
const queryUrl = config.settings.searchUrl || 'https://www.google.com/search?q=';
const textInput: any = useRef(null);
useHotkeys([['ctrl+K', () => textInput.current.focus()]]);
const textInput = useRef<HTMLInputElement>();
useHotkeys([['ctrl+K', () => textInput.current && textInput.current.focus()]]);
const { classes, cx } = useStyles();
const theme = useMantineTheme();
const rightSection = (
<div className={classes.hide}>
<Kbd>Ctrl</Kbd>
@@ -38,7 +45,8 @@ export default function SearchBar(props: any) {
},
});
if (config.settings.searchBar === false) {
// If enabled modules doesn't contain the module, return null
if (!config.settings.enabledModules.includes(SearchModule.title)) {
return null;
}
@@ -58,17 +66,19 @@ export default function SearchBar(props: any) {
}
}}
onSubmit={form.onSubmit((values) => {
// Find if query is prefixed by !yt or !t
const query = values.query.trim();
const isYoutube = query.startsWith('!yt');
const isTorrent = query.startsWith('!t');
if (isYoutube) {
window.open(`https://www.youtube.com/results?search_query=${query.substring(3)}`);
} else if (isTorrent) {
window.open(`https://bitsearch.to/search?q=${query.substring(3)}`);
} else {
window.open(`${queryUrl}${values.query}`);
}
form.setValues({ query: '' });
setTimeout(() => {
if (isYoutube) {
window.open(`https://www.youtube.com/results?search_query=${query.substring(3)}`);
} else if (isTorrent) {
window.open(`https://bitsearch.to/search?q=${query.substring(3)}`);
} else {
window.open(`${queryUrl}${values.query}`);
}
}, 20);
})}
>
<Popover

View File

@@ -0,0 +1 @@
export { SearchModule } from './SearchModule';

View File

@@ -0,0 +1,41 @@
// To parse this data:
//
// import { Convert, WeatherResponse } from "./file";
//
// const weatherResponse = Convert.toWeatherResponse(json);
//
// These functions will throw an error if the JSON doesn't
// match the expected interface, even if the JSON is valid.
export interface WeatherResponse {
current_weather: CurrentWeather;
utc_offset_seconds: number;
latitude: number;
elevation: number;
longitude: number;
generationtime_ms: number;
daily_units: DailyUnits;
daily: Daily;
}
export interface CurrentWeather {
winddirection: number;
windspeed: number;
time: string;
weathercode: number;
temperature: number;
}
export interface Daily {
temperature_2m_max: number[];
time: Date[];
temperature_2m_min: number[];
weathercode: number[];
}
export interface DailyUnits {
temperature_2m_max: string;
temperature_2m_min: string;
time: string;
weathercode: string;
}

View File

@@ -0,0 +1,174 @@
import { Group, Space, Title, Tooltip } from '@mantine/core';
import axios from 'axios';
import { useEffect, useState } from 'react';
import {
ArrowDownRight,
ArrowUpRight,
Cloud,
CloudFog,
CloudRain,
CloudSnow,
CloudStorm,
QuestionMark,
Snowflake,
Sun,
} from 'tabler-icons-react';
import { useConfig } from '../../../tools/state';
import { IModule } from '../modules';
import { WeatherResponse } from './WeatherInterface';
export const WeatherModule: IModule = {
title: 'Weather (beta)',
description: 'Look up the current weather in your location',
icon: Sun,
component: WeatherComponent,
options: {
freedomunit: {
name: 'Display in Fahrenheit',
value: false,
},
},
};
// 0 Clear sky
// 1, 2, 3 Mainly clear, partly cloudy, and overcast
// 45, 48 Fog and depositing rime fog
// 51, 53, 55 Drizzle: Light, moderate, and dense intensity
// 56, 57 Freezing Drizzle: Light and dense intensity
// 61, 63, 65 Rain: Slight, moderate and heavy intensity
// 66, 67 Freezing Rain: Light and heavy intensity
// 71, 73, 75 Snow fall: Slight, moderate, and heavy intensity
// 77 Snow grains
// 80, 81, 82 Rain showers: Slight, moderate, and violent
// 85, 86Snow showers slight and heavy
// 95 *Thunderstorm: Slight or moderate
// 96, 99 *Thunderstorm with slight and heavy hail
export function WeatherIcon(props: any) {
const { code } = props;
let data: { icon: any; name: string };
switch (code) {
case 0: {
data = { icon: Sun, name: 'Clear' };
break;
}
case 1:
case 2:
case 3: {
data = { icon: Cloud, name: 'Mainly clear' };
break;
}
case 45:
case 48: {
data = { icon: CloudFog, name: 'Fog' };
break;
}
case 51:
case 53:
case 55: {
data = { icon: Cloud, name: 'Drizzle' };
break;
}
case 56:
case 57: {
data = { icon: Snowflake, name: 'Freezing drizzle' };
break;
}
case 61:
case 63:
case 65: {
data = { icon: CloudRain, name: 'Rain' };
break;
}
case 66:
case 67: {
data = { icon: CloudRain, name: 'Freezing rain' };
break;
}
case 71:
case 73:
case 75: {
data = { icon: CloudSnow, name: 'Snow fall' };
break;
}
case 77: {
data = { icon: CloudSnow, name: 'Snow grains' };
break;
}
case 80:
case 81:
case 82: {
data = { icon: CloudRain, name: 'Rain showers' };
break;
}
case 85:
case 86: {
data = { icon: CloudSnow, name: 'Snow showers' };
break;
}
case 95: {
data = { icon: CloudStorm, name: 'Thunderstorm' };
break;
}
case 96:
case 99: {
data = { icon: CloudStorm, name: 'Thunderstorm with hail' };
break;
}
default: {
data = { icon: QuestionMark, name: 'Unknown' };
}
}
return (
<Tooltip label={data.name}>
<data.icon size={50} />
</Tooltip>
);
}
export default function WeatherComponent(props: any) {
// Get location from browser
const [location, setLocation] = useState({ lat: 0, lng: 0 });
const { config } = useConfig();
const [weather, setWeather] = useState({} as WeatherResponse);
const isFahrenheit: boolean =
config.settings[`${WeatherModule.title}.freedomunit`] === undefined
? false
: config.settings[`${WeatherModule.title}.freedomunit`];
if ('geolocation' in navigator && location.lat === 0 && location.lng === 0) {
navigator.geolocation.getCurrentPosition((position) => {
setLocation({ lat: position.coords.latitude, lng: position.coords.longitude });
});
}
useEffect(() => {
axios
.get(
`https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lng}&daily=weathercode,temperature_2m_max,temperature_2m_min&current_weather=true&timezone=Europe%2FLondon`
)
.then((res) => {
setWeather(res.data);
});
}, []);
if (!weather.current_weather) {
return null;
}
function usePerferedUnit(value: number): string {
return isFahrenheit ? `${(value * (9 / 5)).toFixed(1)}°F` : `${value.toFixed(1)}°C`;
}
return (
<Group position="left" direction="column">
<Title>{usePerferedUnit(weather.current_weather.temperature)}</Title>
<Group spacing={0}>
<WeatherIcon code={weather.current_weather.weathercode} />
<Space mx="sm" />
<span>{weather.daily.temperature_2m_max[0]}°C</span>
<ArrowUpRight size={16} style={{ right: 15 }} />
<Space mx="sm" />
<span>{weather.daily.temperature_2m_min[0]}°C</span>
<ArrowDownRight size={16} />
</Group>
</Group>
);
}

View File

@@ -0,0 +1 @@
export { WeatherModule } from './WeatherModule';

95
src/pages/404.tsx Normal file
View File

@@ -0,0 +1,95 @@
import React from 'react';
import {
createStyles,
Container,
Title,
Text,
Button,
Group,
useMantineTheme,
} from '@mantine/core';
import { NextLink } from '@mantine/next';
const useStyles = createStyles((theme) => ({
root: {
paddingTop: 80,
paddingBottom: 80,
},
inner: {
position: 'relative',
},
image: {
position: 'absolute',
top: 0,
right: 0,
left: 0,
zIndex: 0,
opacity: 0.75,
},
content: {
paddingTop: 220,
position: 'relative',
zIndex: 1,
[theme.fn.smallerThan('sm')]: {
paddingTop: 120,
},
},
title: {
fontFamily: `Greycliff CF, ${theme.fontFamily}`,
textAlign: 'center',
fontWeight: 900,
fontSize: 38,
[theme.fn.smallerThan('sm')]: {
fontSize: 32,
},
},
description: {
maxWidth: 540,
margin: 'auto',
marginTop: theme.spacing.xl,
marginBottom: theme.spacing.xl * 1.5,
},
}));
export function Illustration(props: React.ComponentPropsWithoutRef<'svg'>) {
const theme = useMantineTheme();
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 362 145" {...props}>
<path
fill={theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0]}
d="M62.6 142c-2.133 0-3.2-1.067-3.2-3.2V118h-56c-2 0-3-1-3-3V92.8c0-1.333.4-2.733 1.2-4.2L58.2 4c.8-1.333 2.067-2 3.8-2h28c2 0 3 1 3 3v85.4h11.2c.933 0 1.733.333 2.4 1 .667.533 1 1.267 1 2.2v21.2c0 .933-.333 1.733-1 2.4-.667.533-1.467.8-2.4.8H93v20.8c0 2.133-1.067 3.2-3.2 3.2H62.6zM33 90.4h26.4V51.2L33 90.4zM181.67 144.6c-7.333 0-14.333-1.333-21-4-6.666-2.667-12.866-6.733-18.6-12.2-5.733-5.467-10.266-13-13.6-22.6-3.333-9.6-5-20.667-5-33.2 0-12.533 1.667-23.6 5-33.2 3.334-9.6 7.867-17.133 13.6-22.6 5.734-5.467 11.934-9.533 18.6-12.2 6.667-2.8 13.667-4.2 21-4.2 7.467 0 14.534 1.4 21.2 4.2 6.667 2.667 12.8 6.733 18.4 12.2 5.734 5.467 10.267 13 13.6 22.6 3.334 9.6 5 20.667 5 33.2 0 12.533-1.666 23.6-5 33.2-3.333 9.6-7.866 17.133-13.6 22.6-5.6 5.467-11.733 9.533-18.4 12.2-6.666 2.667-13.733 4-21.2 4zm0-31c9.067 0 15.6-3.733 19.6-11.2 4.134-7.6 6.2-17.533 6.2-29.8s-2.066-22.2-6.2-29.8c-4.133-7.6-10.666-11.4-19.6-11.4-8.933 0-15.466 3.8-19.6 11.4-4 7.6-6 17.533-6 29.8s2 22.2 6 29.8c4.134 7.467 10.667 11.2 19.6 11.2zM316.116 142c-2.134 0-3.2-1.067-3.2-3.2V118h-56c-2 0-3-1-3-3V92.8c0-1.333.4-2.733 1.2-4.2l56.6-84.6c.8-1.333 2.066-2 3.8-2h28c2 0 3 1 3 3v85.4h11.2c.933 0 1.733.333 2.4 1 .666.533 1 1.267 1 2.2v21.2c0 .933-.334 1.733-1 2.4-.667.533-1.467.8-2.4.8h-11.2v20.8c0 2.133-1.067 3.2-3.2 3.2h-27.2zm-29.6-51.6h26.4V51.2l-26.4 39.2z"
/>
</svg>
);
}
export default function NothingFoundBackground() {
const { classes } = useStyles();
return (
<Container className={classes.root}>
<div className={classes.inner}>
<Illustration className={classes.image} />
<div className={classes.content}>
<Title className={classes.title}>Nothing to see here</Title>
<Text color="dimmed" size="lg" align="center" className={classes.description}>
Page you are trying to open does not exist. You may have mistyped the address, or the
page has been moved to another URL. If you think this is an error contact support.
</Text>
<Group position="center">
<NextLink href="/">
<Button size="md">Take me back to home page</Button>
</NextLink>
</Group>
</div>
</div>
</Container>
);
}

8
src/pages/[slug].tsx Normal file
View File

@@ -0,0 +1,8 @@
import { Title } from '@mantine/core';
import { useRouter } from 'next/router';
export default function SlugPage(props: any) {
const router = useRouter();
const { slug } = router.query;
return <Title>ok</Title>;
}

View File

@@ -24,7 +24,7 @@ export default function App(props: AppProps & { colorScheme: ColorScheme }) {
return (
<>
<Head>
<title>Homarr - A homepage for your server!</title>
<title>Homarr 🦞</title>
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
<link rel="shortcut icon" href="/favicon.svg" />
</Head>

View File

@@ -0,0 +1,29 @@
import axios from 'axios';
import { NextApiRequest, NextApiResponse } from 'next';
async function Get(req: NextApiRequest, res: NextApiResponse) {
// Parse req.body as a ServiceItem
const { url } = req.query;
await axios
.get(url as string)
.then((response) => {
res.status(200).json(response.data);
})
.catch((error) => {
res.status(500).json(error);
});
// // Make a request to the URL
// const response = await axios.get(url);
// // Return the response
}
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',
});
};

View File

@@ -1,6 +1,5 @@
export interface Settings {
searchUrl: string;
searchBar: boolean;
enabledModules: string[];
[key: string]: any;
}

2615
yarn.lock

File diff suppressed because it is too large Load Diff