feat: add hero banner (#463)

This commit is contained in:
Manuel
2024-05-12 18:19:01 +02:00
committed by GitHub
parent 1570faa20a
commit 87fe03dd00
9 changed files with 269 additions and 51 deletions

View File

@@ -0,0 +1,37 @@
.bannerContainer {
padding: 3rem;
border-radius: 8px;
overflow: hidden;
background: linear-gradient(
130deg,
#fa52521f 0%,
var(--mantine-color-dark-6) 35%,
var(--mantine-color-dark-6) 100%
) !important;
}
.scrollContainer {
height: 100%;
transform: rotateZ(10deg);
}
@keyframes scrolling {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-50%);
}
}
.scrollAnimationContainer {
animation: scrolling;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
@media (prefers-reduced-motion: reduce) {
.scrollAnimationContainer {
animation: none !important;
}
}

View File

@@ -0,0 +1,96 @@
import { Box, Grid, GridCol, Group, Image, Stack, Title } from "@mantine/core";
import { splitToNChunks } from "@homarr/common";
import classes from "./hero-banner.module.css";
const icons = [
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/homarr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/sabnzbd.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/deluge.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/radarr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/sonarr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/lidarr.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/pihole.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/dashdot.png",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/overseerr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/plex.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/jellyfin.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/homeassistant.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/freshrss.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/readarr.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/transmission.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/qbittorrent.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nzbget.png",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/openmediavault.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/docker.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/jellyseerr.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/adguardhome.svg",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/tdarr.png",
"https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/svg/prowlarr.svg",
];
const countIconGroups = 3;
const animationDurationInSeconds = 12;
export const HeroBanner = () => {
const arrayInChunks = splitToNChunks(icons, countIconGroups);
const gridSpan = 12 / countIconGroups;
return (
<Box className={classes.bannerContainer} bg="dark.6" pos="relative">
<Stack gap={0}>
<Title order={2} c="dimmed">
Welcome back to your
</Title>
<Group gap="xs">
<Image src="/logo/logo.png" w={40} h={40} />
<Title>Homarr Dashboard</Title>
</Group>
</Stack>
<Box
className={classes.scrollContainer}
w={"30%"}
top={0}
right={0}
pos="absolute"
>
<Grid>
{Array(countIconGroups)
.fill(0)
.map((_, columnIndex) => (
<GridCol key={`grid-column-${columnIndex}`} span={gridSpan}>
<Stack
className={classes.scrollAnimationContainer}
style={{
animationDuration: `${animationDurationInSeconds - columnIndex}s`,
}}
>
{arrayInChunks[columnIndex]?.map((icon, index) => (
<Image
key={`grid-column-${columnIndex}-scroll-1-${index}`}
src={icon}
radius="md"
w={50}
h={50}
/>
))}
{/* This is used for making the animation seem seamless */}
{arrayInChunks[columnIndex]?.map((icon, index) => (
<Image
key={`grid-column-${columnIndex}-scroll-2-${index}`}
src={icon}
radius="md"
w={50}
h={50}
/>
))}
</Stack>
</GridCol>
))}
</Grid>
</Box>
</Box>
);
};

View File

@@ -1,8 +1,18 @@
import { Title } from "@mantine/core";
import Link from "next/link";
import { Card, Group, SimpleGrid, Space, Stack, Text } from "@mantine/core";
import { IconArrowRight } from "@tabler/icons-react";
import { api } from "@homarr/api/server";
import { getScopedI18n } from "@homarr/translation/server";
import { Test } from "./test";
import { HeroBanner } from "./_components/hero-banner";
interface LinkProps {
title: string;
subtitle: string;
count: number;
href: string;
}
export async function generateMetadata() {
const t = await getScopedI18n("management");
@@ -14,20 +24,76 @@ export async function generateMetadata() {
}
export default async function ManagementPage() {
const t = await getScopedI18n("management.title");
const dateNow = new Date();
const timeOfDay =
dateNow.getHours() < 10
? "morning"
: dateNow.getHours() < 17
? "afternoon"
: "evening";
const statistics = await api.home.getStats();
const t = await getScopedI18n("management.page.home");
const links: LinkProps[] = [
{
count: statistics.countBoards,
href: "/manage/boards",
subtitle: t("statisticLabel.boards"),
title: t("statistic.countBoards"),
},
{
count: statistics.countUsers,
href: "/manage/boards",
subtitle: t("statisticLabel.authentication"),
title: t("statistic.createUser"),
},
{
count: statistics.countInvites,
href: "/manage/boards",
subtitle: t("statisticLabel.authentication"),
title: t("statistic.createInvite"),
},
{
count: statistics.countIntegrations,
href: "/manage/integrations",
subtitle: t("statisticLabel.resources"),
title: t("statistic.addIntegration"),
},
{
count: statistics.countApps,
href: "/manage/apps",
subtitle: t("statisticLabel.resources"),
title: t("statistic.addApp"),
},
{
count: statistics.countGroups,
href: "/manage/users/groups",
subtitle: t("statisticLabel.authorization"),
title: t("statistic.manageRoles"),
},
];
return (
<>
<Title>{t(timeOfDay, { username: "admin" })}</Title>
<Test />
<HeroBanner />
<Space h="md" />
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
{links.map((link, index) => (
<Card
component={Link}
href={link.href}
key={`link-${index}`}
withBorder
>
<Group justify="space-between">
<Group>
<Text size="2.4rem" fw="bolder">
{link.count}
</Text>
<Stack gap={0}>
<Text c="red" size="xs">
{link.subtitle}
</Text>
<Text fw="bold">{link.title}</Text>
</Stack>
</Group>
<IconArrowRight />
</Group>
</Card>
))}
</SimpleGrid>
</>
);
}

View File

@@ -1,38 +0,0 @@
"use client";
import { useCallback, useState } from "react";
import type { ChangeEvent } from "react";
import { Button, Stack, Text, TextInput } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
export const Test = () => {
const [value, setValue] = useState("");
const [message, setMessage] = useState<string>("Hello, world!");
const { mutate } = clientApi.user.setMessage.useMutation();
clientApi.user.test.useSubscription(undefined, {
onData({ message }) {
setMessage(message);
},
onError(err) {
alert(err);
},
});
const onChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => setValue(event.target.value),
[setValue],
);
const onClick = useCallback(() => {
mutate(value);
setValue("");
}, [mutate, value]);
return (
<Stack>
<TextInput label="Update message" value={value} onChange={onChange} />
<Button onClick={onClick}>Update message</Button>
<Text>This message gets through subscription: {message}</Text>
</Stack>
);
};

View File

@@ -1,6 +1,7 @@
import { appRouter as innerAppRouter } from "./router/app";
import { boardRouter } from "./router/board";
import { groupRouter } from "./router/group";
import { homeRouter } from "./router/home";
import { iconsRouter } from "./router/icons";
import { integrationRouter } from "./router/integration";
import { inviteRouter } from "./router/invite";
@@ -21,6 +22,7 @@ export const appRouter = createTRPCRouter({
location: locationRouter,
log: logRouter,
icon: iconsRouter,
home: homeRouter,
});
// export type definition of API

View File

@@ -0,0 +1,31 @@
import { count } from "@homarr/db";
import {
apps,
boards,
groups,
integrations,
invites,
users,
} from "@homarr/db/schema/sqlite";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const homeRouter = createTRPCRouter({
getStats: protectedProcedure.query(async ({ ctx }) => {
return {
countBoards:
(await ctx.db.select({ count: count() }).from(boards))[0]?.count ?? 0,
countUsers:
(await ctx.db.select({ count: count() }).from(users))[0]?.count ?? 0,
countGroups:
(await ctx.db.select({ count: count() }).from(groups))[0]?.count ?? 0,
countInvites:
(await ctx.db.select({ count: count() }).from(invites))[0]?.count ?? 0,
countIntegrations:
(await ctx.db.select({ count: count() }).from(integrations))[0]
?.count ?? 0,
countApps:
(await ctx.db.select({ count: count() }).from(apps))[0]?.count ?? 0,
};
}),
});

View File

@@ -0,0 +1,7 @@
export const splitToNChunks = <T>(array: T[], chunks: number): T[][] => {
const result: T[][] = [];
for (let i = chunks; i > 0; i--) {
result.push(array.splice(0, Math.ceil(array.length / i)));
}
return result;
};

View File

@@ -1,4 +1,5 @@
export * from "./object";
export * from "./string";
export * from "./cookie";
export * from "./array";
export * from "./stopwatch";

View File

@@ -1099,6 +1099,22 @@ export default {
},
},
page: {
home: {
statistic: {
countBoards: "Boards",
createUser: "Create new user",
createInvite: "Create new invite",
addIntegration: "Create integration",
addApp: "Add app",
manageRoles: "Manage roles",
},
statisticLabel: {
boards: "Boards",
resources: "Resources",
authentication: "Authentication",
authorization: "Authorization",
},
},
board: {
title: "Your boards",
action: {