From 87fe03dd009be451981f99910d8f88129ecde9d1 Mon Sep 17 00:00:00 2001
From: Manuel <30572287+manuel-rw@users.noreply.github.com>
Date: Sun, 12 May 2024 18:19:01 +0200
Subject: [PATCH] feat: add hero banner (#463)
---
.../manage/_components/hero-banner.module.css | 37 +++++++
.../manage/_components/hero-banner.tsx | 96 +++++++++++++++++++
apps/nextjs/src/app/[locale]/manage/page.tsx | 92 +++++++++++++++---
apps/nextjs/src/app/[locale]/manage/test.tsx | 38 --------
packages/api/src/root.ts | 2 +
packages/api/src/router/home.ts | 31 ++++++
packages/common/src/array.ts | 7 ++
packages/common/src/index.ts | 1 +
packages/translation/src/lang/en.ts | 16 ++++
9 files changed, 269 insertions(+), 51 deletions(-)
create mode 100644 apps/nextjs/src/app/[locale]/manage/_components/hero-banner.module.css
create mode 100644 apps/nextjs/src/app/[locale]/manage/_components/hero-banner.tsx
delete mode 100644 apps/nextjs/src/app/[locale]/manage/test.tsx
create mode 100644 packages/api/src/router/home.ts
create mode 100644 packages/common/src/array.ts
diff --git a/apps/nextjs/src/app/[locale]/manage/_components/hero-banner.module.css b/apps/nextjs/src/app/[locale]/manage/_components/hero-banner.module.css
new file mode 100644
index 000000000..cfadc375d
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/manage/_components/hero-banner.module.css
@@ -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;
+ }
+}
diff --git a/apps/nextjs/src/app/[locale]/manage/_components/hero-banner.tsx b/apps/nextjs/src/app/[locale]/manage/_components/hero-banner.tsx
new file mode 100644
index 000000000..be82bdf1a
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/manage/_components/hero-banner.tsx
@@ -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 (
+
+
+
+ Welcome back to your
+
+
+
+ Homarr Dashboard
+
+
+
+
+ {Array(countIconGroups)
+ .fill(0)
+ .map((_, columnIndex) => (
+
+
+ {arrayInChunks[columnIndex]?.map((icon, index) => (
+
+ ))}
+
+ {/* This is used for making the animation seem seamless */}
+ {arrayInChunks[columnIndex]?.map((icon, index) => (
+
+ ))}
+
+
+ ))}
+
+
+
+ );
+};
diff --git a/apps/nextjs/src/app/[locale]/manage/page.tsx b/apps/nextjs/src/app/[locale]/manage/page.tsx
index 658797c6c..6c1bf2b84 100644
--- a/apps/nextjs/src/app/[locale]/manage/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/page.tsx
@@ -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 (
<>
-
{t(timeOfDay, { username: "admin" })}
-
+
+
+
+ {links.map((link, index) => (
+
+
+
+
+ {link.count}
+
+
+
+ {link.subtitle}
+
+ {link.title}
+
+
+
+
+
+ ))}
+
>
);
}
diff --git a/apps/nextjs/src/app/[locale]/manage/test.tsx b/apps/nextjs/src/app/[locale]/manage/test.tsx
deleted file mode 100644
index de6f55090..000000000
--- a/apps/nextjs/src/app/[locale]/manage/test.tsx
+++ /dev/null
@@ -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("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) => setValue(event.target.value),
- [setValue],
- );
- const onClick = useCallback(() => {
- mutate(value);
- setValue("");
- }, [mutate, value]);
-
- return (
-
-
-
- This message gets through subscription: {message}
-
- );
-};
diff --git a/packages/api/src/root.ts b/packages/api/src/root.ts
index 57da6c59b..0ee76f2aa 100644
--- a/packages/api/src/root.ts
+++ b/packages/api/src/root.ts
@@ -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
diff --git a/packages/api/src/router/home.ts b/packages/api/src/router/home.ts
new file mode 100644
index 000000000..9bcabf029
--- /dev/null
+++ b/packages/api/src/router/home.ts
@@ -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,
+ };
+ }),
+});
diff --git a/packages/common/src/array.ts b/packages/common/src/array.ts
new file mode 100644
index 000000000..823dc7794
--- /dev/null
+++ b/packages/common/src/array.ts
@@ -0,0 +1,7 @@
+export const splitToNChunks = (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;
+};
diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts
index 7b0025e68..1089f3537 100644
--- a/packages/common/src/index.ts
+++ b/packages/common/src/index.ts
@@ -1,4 +1,5 @@
export * from "./object";
export * from "./string";
export * from "./cookie";
+export * from "./array";
export * from "./stopwatch";
diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts
index 5f13b297a..8fec28235 100644
--- a/packages/translation/src/lang/en.ts
+++ b/packages/translation/src/lang/en.ts
@@ -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: {