From 5c1c997fab5ac8c27da4d85071d5810ecf9a45ec Mon Sep 17 00:00:00 2001 From: Tim Ittermann Date: Wed, 7 Aug 2024 21:42:16 +0200 Subject: [PATCH] feat: add first users page --- src/app/[locale]/(userArea)/admin/layout.tsx | 11 ++- .../[locale]/(userArea)/admin/users/page.tsx | 77 +++++++++++++++++++ src/components/common/ActiveLink.tsx | 23 ++++++ src/components/common/PagingComponent.tsx | 32 ++++++++ src/dal/admin/getUsers.ts | 40 ++++++++++ src/locales/de.ts | 4 + src/locales/en.ts | 4 + src/routes.ts | 1 + src/types/PagingResult.ts | 6 ++ 9 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 src/app/[locale]/(userArea)/admin/users/page.tsx create mode 100644 src/components/common/ActiveLink.tsx create mode 100644 src/components/common/PagingComponent.tsx create mode 100644 src/dal/admin/getUsers.ts create mode 100644 src/types/PagingResult.ts diff --git a/src/app/[locale]/(userArea)/admin/layout.tsx b/src/app/[locale]/(userArea)/admin/layout.tsx index 2536975..9372eb6 100644 --- a/src/app/[locale]/(userArea)/admin/layout.tsx +++ b/src/app/[locale]/(userArea)/admin/layout.tsx @@ -1,8 +1,8 @@ +import { ActiveLink } from "@/components/common/ActiveLink"; import { Heading } from "@/components/common/Heading"; import { ensureIsAdmin } from "@/functions/user/ensureIsAdmin"; import { getScopedI18n } from "@/locales/server"; import { getRoute } from "@/routes"; -import Link from "next/link"; import type { ReactElement } from "react"; export default async function AdminLayout({ @@ -20,9 +20,12 @@ export default async function AdminLayout({ {t("title")} diff --git a/src/app/[locale]/(userArea)/admin/users/page.tsx b/src/app/[locale]/(userArea)/admin/users/page.tsx new file mode 100644 index 0000000..2f6dac1 --- /dev/null +++ b/src/app/[locale]/(userArea)/admin/users/page.tsx @@ -0,0 +1,77 @@ +import PagingComponent from "@/components/common/PagingComponent"; +import { ProfileImage } from "@/components/common/ProfileImage"; +import { getUsers } from "@/dal/admin/getUsers"; +import { getCurrentLocale, getScopedI18n } from "@/locales/server"; +import type { User } from "@prisma/client"; + +const pageSize = 50; + +type Props = { + searchParams: { + query: string | undefined; + skip: string | undefined; + }; +}; + +async function UserComponent({ user }: { user: User }) { + const t = await getScopedI18n("admin"); + const locale = getCurrentLocale(); + + return ( +
+
+ +

{user.name}

+
+
+

+ {t("userEmail")}: {user.email} +

+

+ {t("userCreatedAt")}: {user.createdAt.toLocaleString(locale)} +

+
+
+ ); +} + +export default async function UsersPage({ searchParams }: Props) { + const t = await getScopedI18n("admin"); + const skip = searchParams.skip ? parseInt(searchParams.skip) : 0; + const { data: users, total } = await getUsers( + searchParams.query, + skip, + pageSize + ); + + return ( +
+
+
+
+ + +
+
+
+ +
+ {users.map((user) => ( + + ))} +
+ + +
+ ); +} diff --git a/src/components/common/ActiveLink.tsx b/src/components/common/ActiveLink.tsx new file mode 100644 index 0000000..ef3f679 --- /dev/null +++ b/src/components/common/ActiveLink.tsx @@ -0,0 +1,23 @@ +"use client"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import type { FC, PropsWithChildren } from "react"; + +type Props = { + href: string; +}; + +/** A (Route)Link that knows if it's active + */ +export const ActiveLink: FC> = ({ + href, + children, +}) => { + const pathName = usePathname(); + + return ( + + {children} + + ); +}; diff --git a/src/components/common/PagingComponent.tsx b/src/components/common/PagingComponent.tsx new file mode 100644 index 0000000..e03aa14 --- /dev/null +++ b/src/components/common/PagingComponent.tsx @@ -0,0 +1,32 @@ +type Props = { + total: number; + pageSize: number; + skip: number; +}; + +export default function PagingComponent({ total, pageSize, skip }: Props) { + const pages = Math.ceil(total / pageSize); + const currentPage = Math.floor(skip / pageSize) + 1; + + return ( +
+
+
+ {Array.from({ length: pages }, (_, i) => i + 1).map((page) => ( + + ))} +
+
+
+ ); +} diff --git a/src/dal/admin/getUsers.ts b/src/dal/admin/getUsers.ts new file mode 100644 index 0000000..02b562b --- /dev/null +++ b/src/dal/admin/getUsers.ts @@ -0,0 +1,40 @@ +import { prisma } from "@/server/db"; +import type { PagingResult } from "@/types/PagingResult"; +import type { User } from "@prisma/client"; + +/** + * Searches for users (or returns all) + * @param query SearchQuery for Email or Name + * @param skip Items to skip + * @param take Items to take (per page) + * @returns List of users + */ +export async function getUsers( + query: string | undefined, + skip: number, + take: number +): Promise> { + const dbQuery: Parameters[0] = { + skip, + take, + }; + + if (query) { + dbQuery.where = { + OR: [{ email: { contains: query } }, { name: { contains: query } }], + }; + } + + const total = await prisma.user.count({ where: dbQuery.where }); + + const users = await prisma.user.findMany({ + ...dbQuery, + orderBy: { name: "asc" }, + }); + + return { + data: users, + total, + skip, + }; +} diff --git a/src/locales/de.ts b/src/locales/de.ts index 6241f68..bae49e0 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -111,5 +111,9 @@ export default { newUsersThisMonth: "Neue Nutzer diesen Monat", newUsersToday: "Neue Nutzer heute", usersKpis: "Nutzer Statistiken", + userEmail: "E-Mail", + userCreatedAt: "Erstellt am", + usersQuery: "Suche nach Name oder E-Mail", + usersSearch: "Suchen", }, } as const; diff --git a/src/locales/en.ts b/src/locales/en.ts index c4b04a6..e64d04e 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -109,5 +109,9 @@ export default { newUsersThisMonth: "New Users this month", newUsersToday: "New Users today", usersKpis: "Users KPIs", + userEmail: "Email", + userCreatedAt: "Created at", + usersQuery: "Search for name or email", + usersSearch: "Search", }, } as const; diff --git a/src/routes.ts b/src/routes.ts index 6a046fd..df91f27 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -25,6 +25,7 @@ const routes = { withLocale(`/mealPlan/join/${invitationCode}`), profile: () => withLocale("/profile"), admin: () => withLocale("/admin"), + adminUsers: () => withLocale("/admin/users"), }; /** Adds the locale to the route */ diff --git a/src/types/PagingResult.ts b/src/types/PagingResult.ts new file mode 100644 index 0000000..a452fdf --- /dev/null +++ b/src/types/PagingResult.ts @@ -0,0 +1,6 @@ +/** A limited result */ +export type PagingResult = { + skip: number; + total: number; + data: T[]; +};