mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-27 00:40:58 +01:00
feat: add hero banner (#463)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
31
packages/api/src/router/home.ts
Normal file
31
packages/api/src/router/home.ts
Normal 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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
7
packages/common/src/array.ts
Normal file
7
packages/common/src/array.ts
Normal 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;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./object";
|
||||
export * from "./string";
|
||||
export * from "./cookie";
|
||||
export * from "./array";
|
||||
export * from "./stopwatch";
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user