mirror of
https://github.com/ajnart/homarr.git
synced 2026-01-09 17:12:19 +01:00
v0.4.0
Add Weather and Ping module
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
14
README.md
14
README.md
@@ -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)**
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const REPO_URL = 'ajnart/homarr';
|
||||
export const CURRENT_VERSION = 'v0.3.1';
|
||||
export const CURRENT_VERSION = 'v0.4.0';
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
export * from './date';
|
||||
export * from './calendar';
|
||||
export * from './search';
|
||||
export * from './ping';
|
||||
export * from './weather';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
15
src/components/modules/ping/PingModule.story.tsx
Normal file
15
src/components/modules/ping/PingModule.story.tsx
Normal 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} />;
|
||||
59
src/components/modules/ping/PingModule.tsx
Normal file
59
src/components/modules/ping/PingModule.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
src/components/modules/ping/index.ts
Normal file
1
src/components/modules/ping/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { PingModule } from './PingModule';
|
||||
@@ -1,4 +1,4 @@
|
||||
import SearchBar from './SearchBar';
|
||||
import SearchBar from './SearchModule';
|
||||
|
||||
export default {
|
||||
title: 'Search bar',
|
||||
@@ -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
|
||||
1
src/components/modules/search/index.ts
Normal file
1
src/components/modules/search/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { SearchModule } from './SearchModule';
|
||||
41
src/components/modules/weather/WeatherInterface.ts
Normal file
41
src/components/modules/weather/WeatherInterface.ts
Normal 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;
|
||||
}
|
||||
174
src/components/modules/weather/WeatherModule.tsx
Normal file
174
src/components/modules/weather/WeatherModule.tsx
Normal 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¤t_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>
|
||||
);
|
||||
}
|
||||
1
src/components/modules/weather/index.ts
Normal file
1
src/components/modules/weather/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { WeatherModule } from './WeatherModule';
|
||||
95
src/pages/404.tsx
Normal file
95
src/pages/404.tsx
Normal 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
8
src/pages/[slug].tsx
Normal 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>;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
29
src/pages/api/modules/ping.ts
Normal file
29
src/pages/api/modules/ping.ts
Normal 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',
|
||||
});
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
export interface Settings {
|
||||
searchUrl: string;
|
||||
searchBar: boolean;
|
||||
enabledModules: string[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user