Skip to content

Commit

Permalink
feat: add first users page
Browse files Browse the repository at this point in the history
  • Loading branch information
timia2109 committed Aug 7, 2024
1 parent 103893a commit 5c1c997
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 4 deletions.
11 changes: 7 additions & 4 deletions src/app/[locale]/(userArea)/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -20,9 +20,12 @@ export default async function AdminLayout({
<Heading>{t("title")}</Heading>
<ul className="menu w-56 rounded-box bg-base-200">
<li>
<Link href={getRoute("admin")} className="active">
{t("kpis")}
</Link>
<ActiveLink href={getRoute("admin")}>{t("kpis")}</ActiveLink>
</li>
<li>
<ActiveLink href={getRoute("adminUsers")}>
{t("users")}
</ActiveLink>
</li>
</ul>
</div>
Expand Down
77 changes: 77 additions & 0 deletions src/app/[locale]/(userArea)/admin/users/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className="border-e border-s border-t border-accent p-3
first:rounded-t last:rounded-b last:border-b"
>
<div className="flex items-center justify-start gap-3">
<ProfileImage user={user} />
<p>{user.name}</p>
</div>
<div>
<p>
{t("userEmail")}: {user.email}
</p>
<p>
{t("userCreatedAt")}: {user.createdAt.toLocaleString(locale)}
</p>
</div>
</div>
);
}

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 (
<div>
<div className="mb-3">
<form method="get">
<div className="join w-full">
<input
className="input join-item input-bordered w-full"
name="query"
defaultValue={searchParams.query}
placeholder={t("usersQuery")}
/>
<button type="submit" className="btn join-item rounded-r-full">
{t("usersSearch")}
</button>
</div>
</form>
</div>

<div>
{users.map((user) => (
<UserComponent key={user.id} user={user} />
))}
</div>

<PagingComponent total={total} pageSize={pageSize} skip={skip} />
</div>
);
}
23 changes: 23 additions & 0 deletions src/components/common/ActiveLink.tsx
Original file line number Diff line number Diff line change
@@ -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<PropsWithChildren<Props>> = ({
href,
children,
}) => {
const pathName = usePathname();

return (
<Link href={href} className={pathName === href ? "active" : ""}>
{children}
</Link>
);
};
32 changes: 32 additions & 0 deletions src/components/common/PagingComponent.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="mt-3 flex justify-end">
<form method="get">
<div className="join">
{Array.from({ length: pages }, (_, i) => i + 1).map((page) => (
<button
key={page}
name="skip"
value={(page - 1) * pageSize}
type="submit"
className={`btn join-item ${
page === currentPage ? "btn-active" : ""
}`}
>
{page}
</button>
))}
</div>
</form>
</div>
);
}
40 changes: 40 additions & 0 deletions src/dal/admin/getUsers.ts
Original file line number Diff line number Diff line change
@@ -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<PagingResult<User>> {
const dbQuery: Parameters<typeof prisma.user.findMany>[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,
};
}
4 changes: 4 additions & 0 deletions src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
4 changes: 4 additions & 0 deletions src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
1 change: 1 addition & 0 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
6 changes: 6 additions & 0 deletions src/types/PagingResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** A limited result */
export type PagingResult<T> = {
skip: number;
total: number;
data: T[];
};

0 comments on commit 5c1c997

Please sign in to comment.