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: {