diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index a1f5ebd..5075d13 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -5,7 +5,7 @@ updates:
schedule:
interval: "weekly"
groups:
- dependencies:
- dependency-types: "production"
- devDependencies:
- dependency-types: "development"
+ dev-deps:
+ dependency-type: "development"
+ prod-deps:
+ dependency-type: "production"
diff --git a/app/(cms)/cms.css b/app/(cms)/cms.css
index 884cc59..d4b80cf 100644
--- a/app/(cms)/cms.css
+++ b/app/(cms)/cms.css
@@ -54,16 +54,30 @@ p > ul > li {
@apply ml-6
}
+@container (max-width: 600px) {
+ table td:not(:nth-child(1)):not(:nth-child(2)):not(:last-child),
+ table th:not(:nth-child(1)):not(:nth-child(2)):not(:last-child) {
+ display: none;
+ }
+}
+
+@container (max-width: 900px) {
+ table td:not(:nth-child(1)):not(:nth-child(2)):not(:nth-child(3)):not(:last-child),
+ table th:not(:nth-child(1)):not(:nth-child(2)):not(:nth-child(3)):not(:last-child) {
+ display: none;
+ }
+}
+
table {
@apply w-full font-light overflow-scroll
}
th {
- @apply border-0 px-3 py-2 first:rounded-tl-md last:rounded-tr-md text-left text-sm uppercase
+ @apply border-0 p-2 first:rounded-tl-md last:rounded-tr-md text-left text-sm uppercase
}
tr {
- @apply even:bg-gray-100 first:border-t border-0 border-gray-100
+ @apply even:bg-gray-50 first:border-t border-0 border-gray-100
}
thead > tr {
@@ -71,19 +85,27 @@ thead > tr {
}
td {
- @apply border-0 border-b border-gray-300 px-3 py-2 font-normal
+ @apply border-0 border-b border-gray-100 p-2 font-normal
}
.card-base {
- @apply bg-white rounded-lg border border-neutral-200 flex flex-col
+ @apply bg-white rounded-lg border border-neutral-200 flex flex-col h-full
}
-.card-content {
- @apply w-full p-4
+.card-header {
+ @apply w-full px-4 pt-4 flex justify-between
}
.card-title {
- @apply w-full p-4 text-lg font-medium text-gray-700
+ @apply text-lg font-medium text-gray-700
+}
+
+.card-toolbar {
+ @apply flex justify-end gap-4 items-center
+}
+
+.card-content {
+ @apply w-full p-4 flex flex-col gap-4 @container
}
.news-title-shadow {
diff --git a/app/(cms)/cms/api/people/add/route.ts b/app/(cms)/cms/api/people/add/route.ts
new file mode 100644
index 0000000..84b3697
--- /dev/null
+++ b/app/(cms)/cms/api/people/add/route.ts
@@ -0,0 +1,13 @@
+import { NextRequest, NextResponse } from "next/server";
+import { drizzle } from "#/lib/db/drizzle";
+import { people } from "#/lib/db/schema";
+import { revalidatePath } from "next/cache";
+
+export async function POST(request: NextRequest) {
+ const newPerson = await drizzle
+ .insert(people)
+ .values({ id: crypto.randomUUID(), firstName: "", lastName: "", phone: "", email: "", image: "" })
+ .returning();
+ revalidatePath("/cms/people");
+ return NextResponse.json({ type: "success", person: { ...newPerson[0] } });
+}
diff --git a/app/(cms)/cms/api/people/roles/route.ts b/app/(cms)/cms/api/people/roles/route.ts
new file mode 100644
index 0000000..f58a81b
--- /dev/null
+++ b/app/(cms)/cms/api/people/roles/route.ts
@@ -0,0 +1,7 @@
+import { NextRequest, NextResponse } from "next/server";
+import { drizzle } from "#/lib/db/drizzle";
+
+export async function GET(request: NextRequest) {
+ const roles = await drizzle.query.roles.findMany();
+ return NextResponse.json({ roles });
+}
diff --git a/app/(cms)/cms/api/people/route.ts b/app/(cms)/cms/api/people/route.ts
new file mode 100644
index 0000000..214124c
--- /dev/null
+++ b/app/(cms)/cms/api/people/route.ts
@@ -0,0 +1,16 @@
+import { NextRequest, NextResponse } from "next/server";
+import { drizzle } from "#/lib/db/drizzle";
+import { PersonWithRoles } from "#/lib/types/people";
+
+export async function GET(request: NextRequest) {
+ const people = await drizzle.query.people.findMany({
+ with: {
+ peopleToRoles: {
+ with: {
+ roles: true,
+ },
+ },
+ },
+ });
+ return NextResponse.json<{ people: PersonWithRoles[] }>({ people });
+}
diff --git a/app/(cms)/cms/error.tsx b/app/(cms)/cms/error.tsx
new file mode 100644
index 0000000..745daba
--- /dev/null
+++ b/app/(cms)/cms/error.tsx
@@ -0,0 +1,14 @@
+"use client";
+
+import { Alert, AlertTitle } from "@mui/material";
+
+export default function Error({ error }: { error: Error & { digest?: string }; reset: () => void }) {
+ return (
+
+
+ Es ist ein Fehler aufgetreten
+ {error.message}
+
+
+ );
+}
diff --git a/app/(cms)/cms/layout.tsx b/app/(cms)/cms/layout.tsx
index 06e9301..2ed2279 100644
--- a/app/(cms)/cms/layout.tsx
+++ b/app/(cms)/cms/layout.tsx
@@ -1,22 +1,43 @@
"use client";
+import "@fontsource/roboto/300.css";
+import "@fontsource/roboto/400.css";
+import "@fontsource/roboto/500.css";
+import "@fontsource/roboto/700.css";
import { PropsWithChildren, useState } from "react";
import { withPageAuthRequired } from "@auth0/nextjs-auth0/client";
import { Navigation } from "#/components/cms/navigation/Navigation";
import { Header } from "#/components/cms/header/Header";
+import { createTheme, ThemeProvider } from "@mui/material";
+
+const theme = createTheme({
+ palette: {
+ primary: {
+ light: "#3999d9",
+ main: "#057DB1",
+ dark: "#015d98",
+ contrastText: "#fff",
+ },
+ },
+});
export default withPageAuthRequired(function CMSLayout({ children }: PropsWithChildren) {
const [navigationOpen, setNavigationOpen] = useState(false);
return (
-
-
setNavigationOpen(false)} />
- setNavigationOpen(false) : () => {}}>
-
setNavigationOpen(true)} />
- {children}
+
+
+
setNavigationOpen(false)} />
+ className="w-full h-full flex flex-col"
+ onClick={navigationOpen ? () => setNavigationOpen(false) : () => {}}
+ >
+ setNavigationOpen(true)} />
+ {children}
+
+
-
+
);
});
diff --git a/app/(cms)/cms/loading.tsx b/app/(cms)/cms/loading.tsx
new file mode 100644
index 0000000..470b3dc
--- /dev/null
+++ b/app/(cms)/cms/loading.tsx
@@ -0,0 +1,9 @@
+import { LinearProgress } from "@mui/material";
+
+export default function Loading() {
+ return (
+
+
+
+ );
+}
diff --git a/app/(cms)/cms/people/PeopleList.tsx b/app/(cms)/cms/people/PeopleList.tsx
new file mode 100644
index 0000000..952d498
--- /dev/null
+++ b/app/(cms)/cms/people/PeopleList.tsx
@@ -0,0 +1,168 @@
+"use client";
+
+import * as React from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { PersonWithRoles } from "#/lib/types/people";
+import { CellContext, createColumnHelper, getCoreRowModel, TableOptions } from "@tanstack/table-core";
+import { Table } from "#/components/cms/table/Table";
+import { PhoneCell } from "#/components/cms/table/cell/Phone";
+import { MailCell } from "#/components/cms/table/cell/Mail";
+import { RolesCell } from "#/components/cms/table/cell/Roles";
+import { FaRegEdit } from "react-icons/fa";
+import { CardToolbar } from "#/components/cms/card/Card";
+import { debounce } from "lodash";
+import { LuSearch } from "react-icons/lu";
+import { MdOutlineDelete, MdOutlinePersonAdd } from "react-icons/md";
+import { Button, IconButton, LinearProgress } from "@mui/material";
+import { TextField } from "#/components/cms/input/TextField";
+import { deletePerson, readAllPeople } from "#/app/(cms)/cms/people/actions";
+import { useRouter } from "next/navigation";
+import { useHotkeys } from "react-hotkeys-hook";
+
+const COLUMN_HELPER = createColumnHelper();
+
+const COLUMN_FIRSTNAME = COLUMN_HELPER.accessor("firstName", {
+ header: "Vorname",
+ id: "firstName",
+});
+
+const COLUMN_LASTNAME = COLUMN_HELPER.accessor("lastName", {
+ header: "Nachname",
+ id: "lastName",
+});
+
+const COLUMN_EMAIL = COLUMN_HELPER.accessor("email", {
+ header: "E-Mail",
+ id: "email",
+ cell: MailCell,
+});
+
+const COLUMN_PHONE = COLUMN_HELPER.accessor("phone", {
+ header: "Phone",
+ id: "phone",
+ cell: PhoneCell,
+});
+
+const COLUMN_ROLES = COLUMN_HELPER.accessor((person) => person.peopleToRoles.map((p) => p.roles.name), {
+ header: "Rollen",
+ id: "roles",
+ cell: RolesCell,
+});
+
+export default function PeopleList() {
+ const [people, setPeople] = useState(undefined);
+
+ useEffect(() => {
+ async function fetchAllPeople() {
+ const response = await fetch("/cms/api/people");
+ const data = await response.json();
+ setPeople(data.people);
+ }
+ fetchAllPeople();
+ }, []);
+
+ const router = useRouter();
+
+ const [searchTerm, setSearchTerm] = useState();
+ const filteredPeople = useMemo(() => {
+ if (people === undefined) return [];
+
+ if (!searchTerm || searchTerm.trim() === "") return people;
+
+ return people.filter((person) => {
+ const firstName = person.firstName?.toLowerCase();
+ const lastName = person.lastName?.toLowerCase();
+
+ if (firstName?.includes(searchTerm)) return true;
+ if (lastName?.includes(searchTerm)) return true;
+ if (`${firstName} ${lastName}`.includes(searchTerm)) return true;
+ if (person.email?.toLowerCase().includes(searchTerm)) return true;
+ return person.peopleToRoles.some((p) => p.roles.name?.toLowerCase().includes(searchTerm) ?? false);
+ });
+ }, [searchTerm, people]);
+
+ const options: TableOptions = useMemo(
+ () => ({
+ data: filteredPeople,
+ columns: [
+ COLUMN_LASTNAME,
+ COLUMN_FIRSTNAME,
+ COLUMN_EMAIL,
+ COLUMN_PHONE,
+ COLUMN_ROLES,
+ COLUMN_HELPER.display({
+ header: "",
+ id: "actions",
+ cell: (props) => (
+ setPeople((prev) => prev?.filter((person) => person.id !== personId))}
+ />
+ ),
+ }),
+ ],
+ initialState: {
+ sorting: [{ id: "lastName", desc: false }],
+ },
+ getRowId: (person) => person.id,
+ getCoreRowModel: getCoreRowModel(),
+ }),
+ [filteredPeople],
+ );
+
+ const onChange = useCallback(
+ debounce((value: string | null) => setSearchTerm(value?.trim().toLowerCase()), 500),
+ [],
+ );
+
+ const onNewPerson = useCallback(async () => {
+ router.push("/cms/people/add");
+ }, [router]);
+ useHotkeys("alt+n", onNewPerson);
+
+ const refSearch = useRef(null);
+ const focusSearch = useCallback(() => {
+ refSearch.current?.focus();
+ }, []);
+ useHotkeys("alt+s", focusSearch);
+
+ return (
+ <>
+
+
+ } href="/cms/people/add">
+ Neue Person
+
+
+
+ >
+ );
+}
+
+function Actions({
+ onDelete,
+ ...cellContext
+}: CellContext & { onDelete: (personId: string) => void }) {
+ const router = useRouter();
+ return (
+
+ {
+ router.push(`/cms/people/${cellContext.row.original.id}`);
+ }}
+ >
+
+
+ {
+ await deletePerson(cellContext.row.original.id);
+ onDelete(cellContext.row.original.id);
+ }}
+ >
+
+
+
+ );
+}
diff --git a/app/(cms)/cms/people/PeopleListCard.tsx b/app/(cms)/cms/people/PeopleListCard.tsx
new file mode 100644
index 0000000..e2e64a1
--- /dev/null
+++ b/app/(cms)/cms/people/PeopleListCard.tsx
@@ -0,0 +1,15 @@
+import { Card, CardContent, CardHeader, CardTitle } from "#/components/cms/card/Card";
+import PeopleList from "#/app/(cms)/cms/people/PeopleList";
+
+export async function PeopleListCard() {
+ return (
+
+
+ Personen
+
+
+
+
+
+ );
+}
diff --git a/app/(cms)/cms/people/[id]/loading.tsx b/app/(cms)/cms/people/[id]/loading.tsx
new file mode 100644
index 0000000..470b3dc
--- /dev/null
+++ b/app/(cms)/cms/people/[id]/loading.tsx
@@ -0,0 +1,9 @@
+import { LinearProgress } from "@mui/material";
+
+export default function Loading() {
+ return (
+
+
+
+ );
+}
diff --git a/app/(cms)/cms/people/[id]/page.tsx b/app/(cms)/cms/people/[id]/page.tsx
new file mode 100644
index 0000000..5f51637
--- /dev/null
+++ b/app/(cms)/cms/people/[id]/page.tsx
@@ -0,0 +1,41 @@
+import "server-only";
+
+import { PageProps } from "#/lib/page";
+import { drizzle } from "#/lib/db/drizzle";
+import { sql } from "drizzle-orm";
+import { PersonWithRoles } from "#/lib/types/people";
+import { cache } from "react";
+import { notFound } from "next/navigation";
+import { Edit } from "#/app/(cms)/cms/people/[id]/section/Edit";
+import placeholder = sql.placeholder;
+
+const preparedPersonStatement = drizzle.query.people
+ .findFirst({
+ with: {
+ peopleToRoles: {
+ with: { roles: true },
+ where: (peopleToRoles, { eq }) => eq(peopleToRoles.peopleId, placeholder("id")),
+ },
+ },
+ where: (person, { eq }) => eq(person.id, placeholder("id")),
+ })
+ .prepare("get_person");
+
+const getPerson = cache(async (id: string) => {
+ const person = await preparedPersonStatement.execute({ id });
+ if (!person) {
+ notFound();
+ }
+ return person;
+});
+
+export default async function Page({ params }: PageProps<{ id: string }>) {
+ const { id } = await params;
+ const person: PersonWithRoles = await getPerson(id);
+
+ return (
+
+
+
+ );
+}
diff --git a/app/(cms)/cms/people/[id]/section/Details.tsx b/app/(cms)/cms/people/[id]/section/Details.tsx
new file mode 100644
index 0000000..e83c19c
--- /dev/null
+++ b/app/(cms)/cms/people/[id]/section/Details.tsx
@@ -0,0 +1,56 @@
+"use client";
+
+import { PersonWithRoles } from "#/lib/types/people";
+import { MutateResult, TextField, TextFieldMutationVariables } from "#/components/cms/input/TextField";
+import { MdOutlineEmail, MdOutlinePhone } from "react-icons/md";
+import { updatePerson } from "#/app/(cms)/cms/people/actions";
+import { useCallback } from "react";
+import { MutationFunction, useDebouncedMutation } from "#/lib/action";
+import { Alert, AlertTitle, Autocomplete, TextField as MuiTextField } from "@mui/material";
+import { Roles } from "#/app/(cms)/cms/people/[id]/section/Roles";
+
+export function Details({ person }: { person: PersonWithRoles }) {
+ const preparedMutateFirstNameFn: MutationFunction = useCallback(
+ async ({ value }: TextFieldMutationVariables): Promise =>
+ updatePerson(person.id, { firstName: value ?? "" }),
+ [person.id],
+ );
+ const mutateFirstName = useDebouncedMutation(preparedMutateFirstNameFn);
+
+ const preparedMutateLastNameFn: MutationFunction = useCallback(
+ async ({ value }: TextFieldMutationVariables): Promise =>
+ updatePerson(person.id, { lastName: value ?? "" }),
+ [person.id],
+ );
+ const mutateLastName = useDebouncedMutation(preparedMutateLastNameFn);
+
+ const preparedMutateEmailFn: MutationFunction = useCallback(
+ async ({ value }: TextFieldMutationVariables): Promise =>
+ updatePerson(person.id, { email: value ?? "" }),
+ [person.id],
+ );
+ const mutateEmail = useDebouncedMutation(preparedMutateEmailFn);
+
+ const preparedMutatePhoneFn: MutationFunction = useCallback(
+ async ({ value }: TextFieldMutationVariables): Promise =>
+ updatePerson(person.id, { phone: value ?? "" }),
+ [person.id],
+ );
+ const mutatePhone = useDebouncedMutation(preparedMutatePhoneFn);
+
+ return (
+
+
+
+
+
+
+
+
+
+ ID
+ {person.id}
+
+
+ );
+}
diff --git a/app/(cms)/cms/people/[id]/section/Edit.tsx b/app/(cms)/cms/people/[id]/section/Edit.tsx
new file mode 100644
index 0000000..a067960
--- /dev/null
+++ b/app/(cms)/cms/people/[id]/section/Edit.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import { PersonWithRoles } from "#/lib/types/people";
+import { useState } from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "#/components/cms/card/Card";
+import { Picture } from "#/app/(cms)/cms/people/[id]/section/Picture";
+import { Details } from "#/app/(cms)/cms/people/[id]/section/Details";
+import { Preview } from "#/app/(cms)/cms/people/[id]/section/Preview";
+
+export function Edit({ person: initialPerson }: { person: PersonWithRoles }) {
+ const [person] = useState(initialPerson);
+
+ return (
+
+
+
+
+ Picture
+
+
+
+
+
+
+
+
+
+ Details
+
+
+
+
+
+
+
+
+
+ Preview
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/(cms)/cms/people/[id]/section/Picture.tsx b/app/(cms)/cms/people/[id]/section/Picture.tsx
new file mode 100644
index 0000000..2802bdc
--- /dev/null
+++ b/app/(cms)/cms/people/[id]/section/Picture.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import NextImage from "next/image";
+import { PersonWithRoles } from "#/lib/types/people";
+import { MutateResult, TextField, TextFieldMutationVariables } from "#/components/cms/input/TextField";
+import { BsPersonBoundingBox } from "react-icons/bs";
+import { SiCloudinary } from "react-icons/si";
+import { MutationFunction, useDebouncedMutation } from "#/lib/action";
+import { useCallback, useState } from "react";
+import { updatePerson } from "#/app/(cms)/cms/people/actions";
+
+function validateImageUrl(url: string): boolean {
+ try {
+ new URL(url);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function Picture({ person }: { person: PersonWithRoles }) {
+ const [imageUrl, setImageUrl] = useState(person.image);
+
+ const preparedMutatePictureFn: MutationFunction = useCallback(
+ async ({ value }: TextFieldMutationVariables): Promise =>
+ updatePerson(person.id, { image: value ?? "" }),
+ [person.id],
+ );
+ const mutateFirstName = useDebouncedMutation((variables) => {
+ if (variables.value !== null) {
+ // synchronously load image and test if it is valid
+ }
+ return preparedMutatePictureFn(variables);
+ });
+ return (
+
+ {imageUrl ? (
+
+ ) : (
+
+ )}
+ {
+ if (value === null || value.trim() === "") {
+ setImageUrl(null);
+ } else if (validateImageUrl(value)) {
+ setImageUrl(value);
+ }
+ }}
+ mutate={mutateFirstName}
+ StartIcon={SiCloudinary}
+ />
+
+ );
+}
diff --git a/app/(cms)/cms/people/[id]/section/Preview.tsx b/app/(cms)/cms/people/[id]/section/Preview.tsx
new file mode 100644
index 0000000..252159d
--- /dev/null
+++ b/app/(cms)/cms/people/[id]/section/Preview.tsx
@@ -0,0 +1,22 @@
+import { PersonWithRoles } from "#/lib/types/people";
+import { PersonCard } from "#/components/person/PersonCard";
+import { Person } from "#/content/people";
+
+export function Preview({ person }: { person: PersonWithRoles }) {
+ const mappedPerson: Person = {
+ firstname: person.firstName ?? "",
+ lastname: person.lastName ?? "",
+ email: person.email ?? "",
+ image: person.image
+ ? {
+ src: person.image ?? "",
+ alt: `${person.firstName} ${person.lastName}`,
+ width: 100,
+ height: 100,
+ }
+ : undefined,
+ phone: [person.phone ?? ""],
+ tags: person.peopleToRoles.map((p) => p.roles.name ?? ""),
+ };
+ return ;
+}
diff --git a/app/(cms)/cms/people/[id]/section/Roles.tsx b/app/(cms)/cms/people/[id]/section/Roles.tsx
new file mode 100644
index 0000000..56ecd3f
--- /dev/null
+++ b/app/(cms)/cms/people/[id]/section/Roles.tsx
@@ -0,0 +1,80 @@
+import { PersonWithRoles, Role } from "#/lib/types/people";
+import { Autocomplete, CircularProgress, TextField } from "@mui/material";
+import { useState } from "react";
+import fetch from "node-fetch";
+import { assignRole, createRole, unassignRole } from "#/app/(cms)/cms/people/actions";
+
+export function Roles({ person }: { person: PersonWithRoles }) {
+ const [open, setOpen] = useState(false);
+ const [options, setOptions] = useState();
+ const [optionsLoading, setOptionsLoading] = useState(false);
+
+ const onOpen = async () => {
+ setOpen(true);
+ if (!options) {
+ setOptionsLoading(true);
+ const response = await fetch("/cms/api/people/roles");
+ const data = await response.json();
+ setOptions([...data.roles].sort((a: Role, b: Role) => (a.name ?? "").localeCompare(b.name ?? "")));
+ setOptionsLoading(false);
+ }
+ };
+
+ const [roles, setRoles] = useState(() =>
+ person.peopleToRoles.map((p) => ({ id: p.roleId, name: p.roles.name })),
+ );
+
+ return (
+ setOpen(false)}
+ loading={optionsLoading}
+ value={roles}
+ onChange={async (_, selectedRoles, reason, details) => {
+ if (reason === "createOption" && typeof details?.option === "string") {
+ const newRoleName: string = details.option;
+ if (!options?.some((r) => r.name?.toLowerCase() === newRoleName.toLowerCase())) {
+ const newRole = await createRole({ name: details.option });
+ await assignRole(person.id, newRole.role.id);
+ setRoles(selectedRoles.map((r) => (typeof r === "string" ? newRole.role : r)));
+ }
+ } else if (reason === "selectOption" && details?.option.id) {
+ await assignRole(person.id, details?.option.id);
+ setRoles(selectedRoles as Role[]);
+ } else if (reason === "removeOption" && details?.option.id) {
+ await unassignRole(person.id, details?.option.id);
+ setRoles(selectedRoles as Role[]);
+ }
+ }}
+ renderInput={(params) => (
+
+ {optionsLoading ? : null}
+ {params.InputProps.endAdornment}
+ >
+ ),
+ },
+ }}
+ />
+ )}
+ getOptionLabel={(option) => (typeof option === "string" ? option : (option.name ?? ""))}
+ options={options ?? []}
+ isOptionEqualToValue={(option, value) => option.id === value.id}
+ filterOptions={(options, params) =>
+ options?.filter?.((option) => option.name?.toLowerCase().includes(params.inputValue.toLowerCase())) ?? []
+ }
+ />
+ );
+}
diff --git a/app/(cms)/cms/people/actions.ts b/app/(cms)/cms/people/actions.ts
new file mode 100644
index 0000000..6b2e9d7
--- /dev/null
+++ b/app/(cms)/cms/people/actions.ts
@@ -0,0 +1,42 @@
+"use server";
+
+import { MutateResult } from "#/components/cms/input/TextField";
+import { drizzle } from "#/lib/db/drizzle";
+import { people, peopleToRoles, roles } from "#/lib/db/schema";
+import { and, eq } from "drizzle-orm";
+import { Person, PersonWithRoles, Role } from "#/lib/types/people";
+import { revalidatePath } from "next/cache";
+
+export const readAllPeople = async (): Promise => {
+ return await drizzle.query.people.findMany({ with: { peopleToRoles: { with: { roles: true } } } });
+};
+
+export const updatePerson = async (id: string, person: Partial>): Promise => {
+ await drizzle.update(people).set(person).where(eq(people.id, id));
+ revalidatePath("/cms/api/people");
+ return { type: "success" };
+};
+
+export const deletePerson = async (id: string): Promise => {
+ await drizzle.delete(people).where(eq(people.id, id));
+ revalidatePath("/cms/api/people");
+ return { type: "success" };
+};
+
+export const createRole = async (role: Omit = { name: "" }): Promise => {
+ const newRole = await drizzle.insert(roles).values({ id: crypto.randomUUID(), name: role.name }).returning();
+ return { type: "success", role: { ...newRole[0] } };
+};
+
+export const assignRole = async (peopleId: string, roleId: string): Promise => {
+ await drizzle.insert(peopleToRoles).values({ peopleId, roleId }).onConflictDoNothing().returning();
+ revalidatePath("/cms/api/people");
+ return { type: "success" };
+};
+
+export const unassignRole = async (peopleId: string, roleId: string): Promise => {
+ await drizzle
+ .delete(peopleToRoles)
+ .where(and(eq(peopleToRoles.peopleId, peopleId), eq(peopleToRoles.roleId, roleId)));
+ return { type: "success" };
+};
diff --git a/app/(cms)/cms/people/add/loading.tsx b/app/(cms)/cms/people/add/loading.tsx
new file mode 100644
index 0000000..470b3dc
--- /dev/null
+++ b/app/(cms)/cms/people/add/loading.tsx
@@ -0,0 +1,9 @@
+import { LinearProgress } from "@mui/material";
+
+export default function Loading() {
+ return (
+
+
+
+ );
+}
diff --git a/app/(cms)/cms/people/add/page.tsx b/app/(cms)/cms/people/add/page.tsx
new file mode 100644
index 0000000..975eb66
--- /dev/null
+++ b/app/(cms)/cms/people/add/page.tsx
@@ -0,0 +1,13 @@
+import { redirect } from "next/navigation";
+import { getURL } from "#/lib/url";
+
+export default async function CreatePersonPage() {
+ const response = await fetch(`${getURL()}/cms/api/people/add`, { method: "POST", cache: "no-store" });
+
+ if (!response.ok) {
+ throw new Error("Person konnte nicht angelegt werden.");
+ }
+
+ const result = await response.json();
+ redirect(`/cms/people/${result.person.id}`);
+}
diff --git a/app/(cms)/cms/people/page.tsx b/app/(cms)/cms/people/page.tsx
index 237b571..95327d8 100644
--- a/app/(cms)/cms/people/page.tsx
+++ b/app/(cms)/cms/people/page.tsx
@@ -1,9 +1,5 @@
-import { Card, CardTitle } from "#/components/cms/card/Card";
+import { PeopleListCard } from "#/app/(cms)/cms/people/PeopleListCard";
-export default function People() {
- return (
-
- Personen
-
- );
+export default async function PeoplePage() {
+ return ;
}
diff --git a/app/(web)/article/[slug]/page.tsx b/app/(web)/article/[slug]/page.tsx
index 4aa4088..5613a90 100644
--- a/app/(web)/article/[slug]/page.tsx
+++ b/app/(web)/article/[slug]/page.tsx
@@ -1,13 +1,13 @@
import Article from "#/components/articles/Article";
import { PageBase } from "#/components/page/PageBase";
-import { getTitle, PageProps } from "#/lib/page";
+import { getTitle, PageProps, Slug } from "#/lib/page";
import { getAllArticleSlugs, getArticleBySlug } from "#/content/article";
import { Metadata } from "next";
import { club } from "#/content/club";
const ARTICLE_DIRECTORY = "public/content/article";
-export async function generateMetadata(props: PageProps): Promise {
+export async function generateMetadata(props: PageProps): Promise {
const params = await props.params;
const article = getArticleBySlug(params.slug, ARTICLE_DIRECTORY);
if (!article) return { title: "Artikel nicht gefunden" };
@@ -36,7 +36,7 @@ export async function generateMetadata(props: PageProps): Promise {
};
}
-export default async function Page(props: PageProps) {
+export default async function Page(props: PageProps) {
const params = await props.params;
const article = getArticleBySlug(params.slug, ARTICLE_DIRECTORY);
return (
diff --git a/app/(web)/event/[slug]/page.tsx b/app/(web)/event/[slug]/page.tsx
index 0ad0ae3..09bc247 100644
--- a/app/(web)/event/[slug]/page.tsx
+++ b/app/(web)/event/[slug]/page.tsx
@@ -1,12 +1,12 @@
import { Metadata } from "next";
-import { getTitle, PageProps } from "#/lib/page";
+import { getTitle, PageProps, Slug } from "#/lib/page";
import { PageBase } from "#/components/page/PageBase";
import EventArticle from "#/components/articles/EventArticle";
import { getAllEventSlugs, getEventBySlug } from "#/content/events";
const EVENT_FOLDER = "public/content/event";
-export async function generateMetadata(props: PageProps): Promise {
+export async function generateMetadata(props: PageProps): Promise {
const params = await props.params;
const article = getEventBySlug(params.slug, EVENT_FOLDER);
return {
@@ -14,7 +14,7 @@ export async function generateMetadata(props: PageProps): Promise {
};
}
-export default async function Page(props: PageProps) {
+export default async function Page(props: PageProps) {
const params = await props.params;
const eventArticle = getEventBySlug(params.slug, EVENT_FOLDER);
return (
diff --git a/components/cms/card/Card.tsx b/components/cms/card/Card.tsx
index c2f7b72..510e9d0 100644
--- a/components/cms/card/Card.tsx
+++ b/components/cms/card/Card.tsx
@@ -4,6 +4,14 @@ export function Card({ children }: PropsWithChildren<{}>) {
return {children}
;
}
+export function CardHeader({ children }: PropsWithChildren<{}>) {
+ return {children}
;
+}
+
+export function CardToolbar({ children }: PropsWithChildren<{}>) {
+ return {children}
;
+}
+
export function CardContent({ children }: PropsWithChildren<{}>) {
return {children}
;
}
diff --git a/components/cms/input/TextField.tsx b/components/cms/input/TextField.tsx
new file mode 100644
index 0000000..cb2ece4
--- /dev/null
+++ b/components/cms/input/TextField.tsx
@@ -0,0 +1,75 @@
+import * as React from "react";
+import { InputAdornment, TextField as MuiTextField, TextFieldProps as MuiTextFieldProps } from "@mui/material";
+import { IconType } from "react-icons";
+import { ChangeEventHandler, useCallback, useState } from "react";
+import { MdCheck, MdError } from "react-icons/md";
+
+export type TextFieldMutationVariables = { value: string | null };
+export type MutateResult = { type: "success" } | { type: "error"; message: string };
+export type MutateFn = (variables: TextFieldMutationVariables) => Promise;
+
+export interface TextFieldProps extends Pick {
+ StartIcon?: IconType;
+ label?: string;
+ mutate?: MutateFn;
+ onChange?: (value: string) => void;
+}
+
+export function TextField({
+ StartIcon,
+ label,
+ defaultValue,
+ mutate,
+ onChange,
+ fullWidth = true,
+ ref,
+ inputRef,
+}: TextFieldProps) {
+ const [value, setValue] = useState(defaultValue ?? "");
+ const [mutationResult, setMutationResult] = useState();
+
+ const onChangeHandler: ChangeEventHandler = useCallback(
+ async (event) => {
+ setMutationResult(undefined);
+ const newValue = event.target.value;
+ setValue(newValue);
+ onChange?.(newValue);
+ setMutationResult(await mutate?.({ value: newValue }));
+ },
+ [mutate, onChange],
+ );
+
+ return (
+ {
+ if (event.key === "Escape") {
+ setValue(defaultValue ?? "");
+ onChange?.(defaultValue?.toString() ?? "");
+ }
+ }}
+ size="small"
+ fullWidth={fullWidth}
+ label={label}
+ aria-label={label}
+ slotProps={{
+ input: {
+ startAdornment: StartIcon ? (
+
+
+
+ ) : null,
+ endAdornment: mutationResult && (
+
+ {mutationResult?.type === "success" && }
+ {mutationResult?.type === "error" && }
+
+ ),
+ },
+ }}
+ />
+ );
+}
diff --git a/components/cms/navigation/Navigation.tsx b/components/cms/navigation/Navigation.tsx
index 7e330f1..39f36de 100644
--- a/components/cms/navigation/Navigation.tsx
+++ b/components/cms/navigation/Navigation.tsx
@@ -1,5 +1,3 @@
-"use client";
-
import Image from "next/image";
import {
MdClose,
@@ -13,6 +11,7 @@ import { Item } from "#/components/cms/navigation/Item";
import { SVWIcon } from "#/components/cms/navigation/SVWIcon";
import { Group } from "#/components/cms/navigation/Group";
import { NavElement } from "#/components/cms/navigation/types";
+import { Fragment } from "react";
const elements: NavElement[] = [
{ type: "item", title: "Dashboard", href: "/cms", Icon: MdOutlineDashboard },
@@ -32,9 +31,9 @@ const elements: NavElement[] = [
export function Navigation({ open, onClose }: { open: boolean; onClose?: () => void }) {
return (
-
+
SVW CMS
@@ -46,12 +45,12 @@ export function Navigation({ open, onClose }: { open: boolean; onClose?: () => v
element.type === "item" ? (
) : (
- <>
-
+
+
{element.items.map((item) => (
))}
- >
+
),
)}
diff --git a/components/cms/table/Table.tsx b/components/cms/table/Table.tsx
index 35dbb9e..2d0e79e 100644
--- a/components/cms/table/Table.tsx
+++ b/components/cms/table/Table.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import { RowData, TableOptions } from "@tanstack/table-core";
import { TableHead } from "#/components/cms/table/TableHead";
import { useReactTable } from "@tanstack/react-table";
diff --git a/components/cms/table/TableBody.tsx b/components/cms/table/TableBody.tsx
index 78f9e22..a4db7bb 100644
--- a/components/cms/table/TableBody.tsx
+++ b/components/cms/table/TableBody.tsx
@@ -1,13 +1,16 @@
import { RowData } from "@tanstack/table-core";
import { PropsWithTable } from "#/components/cms/table/types";
import { TableBodyRow } from "#/components/cms/table/TableBodyRow";
+import { LinearProgress } from "@mui/material";
export function TableBody
({ table, loading }: PropsWithTable & { loading: boolean }) {
return (
{loading && (
- | Lädt... |
+
+
+ |
)}
{!loading && table.getRowModel().rows.map((row) => )}
diff --git a/components/cms/table/cell/Mail.tsx b/components/cms/table/cell/Mail.tsx
new file mode 100644
index 0000000..b344f6b
--- /dev/null
+++ b/components/cms/table/cell/Mail.tsx
@@ -0,0 +1,7 @@
+import { CellContext, RowData } from "@tanstack/table-core";
+import Link from "next/link";
+
+export function MailCell({ cell }: CellContext) {
+ const value = cell.getValue();
+ return value ? {value} : null;
+}
diff --git a/components/cms/table/cell/Phone.tsx b/components/cms/table/cell/Phone.tsx
new file mode 100644
index 0000000..b336c22
--- /dev/null
+++ b/components/cms/table/cell/Phone.tsx
@@ -0,0 +1,11 @@
+import { CellContext, RowData } from "@tanstack/table-core";
+import Link from "next/link";
+
+export function PhoneCell({ cell }: CellContext) {
+ const value = cell.getValue();
+ return value ? (
+
+ {value}
+
+ ) : null;
+}
diff --git a/components/cms/table/cell/Roles.tsx b/components/cms/table/cell/Roles.tsx
new file mode 100644
index 0000000..5b30b8d
--- /dev/null
+++ b/components/cms/table/cell/Roles.tsx
@@ -0,0 +1,22 @@
+import { CellContext, RowData } from "@tanstack/table-core";
+
+export function RolesCell({ cell }: CellContext) {
+ const value = cell.getValue();
+
+ if (value) {
+ return (
+
+ {value.map((role) => (
+
+ {role}
+
+ ))}
+
+ );
+ }
+
+ return null;
+}
diff --git a/components/person/PersonCard.tsx b/components/person/PersonCard.tsx
index 84398c5..d69c2f5 100644
--- a/components/person/PersonCard.tsx
+++ b/components/person/PersonCard.tsx
@@ -2,7 +2,7 @@ import Image from "next/image";
import Link from "next/link";
import { PropsWithChildren } from "react";
import { getPersonName, Person } from "#/content/people";
-import { GoPersonFill as Placeholder } from "react-icons/go";
+import { BsPersonBoundingBox as Placeholder } from "react-icons/bs";
import { calcImageDimensionsForWidth } from "#/lib/image";
type Props = {
diff --git a/lib/action.ts b/lib/action.ts
new file mode 100644
index 0000000..2d2b935
--- /dev/null
+++ b/lib/action.ts
@@ -0,0 +1,39 @@
+import { useCallback, useRef } from "react";
+import { debounce } from "lodash";
+
+export type MutationFunction = (variables: TVariables) => Promise;
+
+export const DEBOUNCE_DEFAULT_WAIT = 500;
+
+export function useDebouncedMutation(
+ mutationFn: MutationFunction,
+) {
+ const promiseRef = useRef<{
+ resolve: (value: TResult) => void;
+ reject: (reason?: any) => void;
+ } | null>(null);
+
+ const debouncedMutation = useCallback(
+ debounce(async (variables: TVariables) => {
+ try {
+ const result = await mutationFn(variables);
+ promiseRef.current?.resolve(result);
+ } catch (error) {
+ promiseRef.current?.reject(error);
+ } finally {
+ promiseRef.current = null;
+ }
+ }, DEBOUNCE_DEFAULT_WAIT),
+ [mutationFn],
+ );
+
+ return useCallback(
+ (variables: TVariables): Promise => {
+ return new Promise((resolve, reject) => {
+ promiseRef.current = { resolve, reject };
+ debouncedMutation(variables);
+ });
+ },
+ [debouncedMutation],
+ );
+}
diff --git a/lib/db/schema/people.ts b/lib/db/schema/people.ts
index ebc330f..a646bc5 100644
--- a/lib/db/schema/people.ts
+++ b/lib/db/schema/people.ts
@@ -3,8 +3,8 @@ import { relations } from "drizzle-orm";
export const people = pgTable("people", {
id: uuid().primaryKey(),
- firstName: text("first_name").notNull(),
- lastName: text("last_name").notNull(),
+ firstName: text("first_name"),
+ lastName: text("last_name"),
email: text(),
phone: text(),
image: text(),
@@ -12,7 +12,7 @@ export const people = pgTable("people", {
export const roles = pgTable("roles", {
id: uuid().primaryKey(),
- name: text().notNull(),
+ name: text(),
});
export const peopleToRoles = pgTable(
@@ -20,10 +20,10 @@ export const peopleToRoles = pgTable(
{
peopleId: uuid("people_id")
.notNull()
- .references(() => people.id),
+ .references(() => people.id, { onDelete: "cascade" }),
roleId: uuid("role_id")
.notNull()
- .references(() => roles.id),
+ .references(() => roles.id, { onDelete: "cascade" }),
},
(t) => [index("pk").on(t.peopleId, t.roleId)],
);
diff --git a/lib/page.ts b/lib/page.ts
index 89edcfc..fe8c96a 100644
--- a/lib/page.ts
+++ b/lib/page.ts
@@ -1,9 +1,13 @@
import { club } from "#/content/club";
-export type PageProps = {
- params: Promise<{ slug: string }>;
+export interface Slug {
+ slug: string;
+}
+
+export interface PageProps {
+ params: Promise;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
-};
+}
export function getTitle(prefix?: string) {
return `${prefix ? `${prefix} - ` : ""}${club.short}`;
diff --git a/lib/types/people.ts b/lib/types/people.ts
new file mode 100644
index 0000000..22dd8a2
--- /dev/null
+++ b/lib/types/people.ts
@@ -0,0 +1,21 @@
+export type Person = {
+ id: string;
+ firstName: string | null;
+ lastName: string | null;
+ email: string | null;
+ phone: string | null;
+ image: string | null;
+};
+
+export type Role = {
+ id: string;
+ name: string | null;
+};
+
+export type PersonWithRoles = Person & {
+ peopleToRoles: {
+ peopleId: string;
+ roleId: string;
+ roles: Role;
+ }[];
+};
diff --git a/migrations/0001_nullable_names.sql b/migrations/0001_nullable_names.sql
new file mode 100644
index 0000000..5e7b6aa
--- /dev/null
+++ b/migrations/0001_nullable_names.sql
@@ -0,0 +1,3 @@
+ALTER TABLE "people" ALTER COLUMN "first_name" DROP NOT NULL;--> statement-breakpoint
+ALTER TABLE "people" ALTER COLUMN "last_name" DROP NOT NULL;--> statement-breakpoint
+ALTER TABLE "roles" ALTER COLUMN "name" DROP NOT NULL;
\ No newline at end of file
diff --git a/migrations/0002_onDelete_cascade.sql b/migrations/0002_onDelete_cascade.sql
new file mode 100644
index 0000000..88ea2b2
--- /dev/null
+++ b/migrations/0002_onDelete_cascade.sql
@@ -0,0 +1,6 @@
+ALTER TABLE "people_to_roles" DROP CONSTRAINT "people_to_roles_people_id_people_id_fk";
+--> statement-breakpoint
+ALTER TABLE "people_to_roles" DROP CONSTRAINT "people_to_roles_role_id_roles_id_fk";
+--> statement-breakpoint
+ALTER TABLE "people_to_roles" ADD CONSTRAINT "people_to_roles_people_id_people_id_fk" FOREIGN KEY ("people_id") REFERENCES "public"."people"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "people_to_roles" ADD CONSTRAINT "people_to_roles_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;
\ No newline at end of file
diff --git a/migrations/meta/0001_snapshot.json b/migrations/meta/0001_snapshot.json
new file mode 100644
index 0000000..0543dca
--- /dev/null
+++ b/migrations/meta/0001_snapshot.json
@@ -0,0 +1,167 @@
+{
+ "id": "8efaa223-9c4f-4f33-963e-df5579234258",
+ "prevId": "bbb6735e-83a9-4305-8e51-09aa86e1bde7",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.people": {
+ "name": "people",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "first_name": {
+ "name": "first_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_name": {
+ "name": "last_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.people_to_roles": {
+ "name": "people_to_roles",
+ "schema": "",
+ "columns": {
+ "people_id": {
+ "name": "people_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "pk": {
+ "name": "pk",
+ "columns": [
+ {
+ "expression": "people_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "role_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "people_to_roles_people_id_people_id_fk": {
+ "name": "people_to_roles_people_id_people_id_fk",
+ "tableFrom": "people_to_roles",
+ "tableTo": "people",
+ "columnsFrom": [
+ "people_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "people_to_roles_role_id_roles_id_fk": {
+ "name": "people_to_roles_role_id_roles_id_fk",
+ "tableFrom": "people_to_roles",
+ "tableTo": "roles",
+ "columnsFrom": [
+ "role_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.roles": {
+ "name": "roles",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/migrations/meta/0002_snapshot.json b/migrations/meta/0002_snapshot.json
new file mode 100644
index 0000000..1e35708
--- /dev/null
+++ b/migrations/meta/0002_snapshot.json
@@ -0,0 +1,167 @@
+{
+ "id": "93b788b6-411f-4633-bafe-fcf6ac076350",
+ "prevId": "8efaa223-9c4f-4f33-963e-df5579234258",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.people": {
+ "name": "people",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "first_name": {
+ "name": "first_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_name": {
+ "name": "last_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.people_to_roles": {
+ "name": "people_to_roles",
+ "schema": "",
+ "columns": {
+ "people_id": {
+ "name": "people_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "pk": {
+ "name": "pk",
+ "columns": [
+ {
+ "expression": "people_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "role_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "people_to_roles_people_id_people_id_fk": {
+ "name": "people_to_roles_people_id_people_id_fk",
+ "tableFrom": "people_to_roles",
+ "tableTo": "people",
+ "columnsFrom": [
+ "people_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "people_to_roles_role_id_roles_id_fk": {
+ "name": "people_to_roles_role_id_roles_id_fk",
+ "tableFrom": "people_to_roles",
+ "tableTo": "roles",
+ "columnsFrom": [
+ "role_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.roles": {
+ "name": "roles",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json
index 5615e1a..59ca0b0 100644
--- a/migrations/meta/_journal.json
+++ b/migrations/meta/_journal.json
@@ -8,6 +8,20 @@
"when": 1734702523610,
"tag": "0000_initial_people_and_roles",
"breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "7",
+ "when": 1735905577738,
+ "tag": "0001_nullable_names",
+ "breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "7",
+ "when": 1738332589470,
+ "tag": "0002_onDelete_cascade",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/next.config.js b/next.config.js
index 23f5964..4594cae 100644
--- a/next.config.js
+++ b/next.config.js
@@ -14,6 +14,12 @@ const nextConfig = {
port: "",
pathname: "/svwalddorf/**",
},
+ {
+ protocol: "https",
+ hostname: "res.cloudinary.com",
+ port: "",
+ pathname: "/svwalddorf/**",
+ },
],
},
webpack: (config) => {
diff --git a/package.json b/package.json
index 69ff310..b73c9dd 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,10 @@
},
"dependencies": {
"@auth0/nextjs-auth0": "^3.5.0",
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.0",
+ "@fontsource/roboto": "^5.1.1",
+ "@mui/material": "^6.4.0",
"@neondatabase/serverless": "^0.10.4",
"@next/mdx": "15.1.0",
"@react-pdf-viewer/core": "^3.12.0",
@@ -32,6 +36,7 @@
"dotenv": "^16.4.7",
"drizzle-orm": "^0.38.2",
"gray-matter": "^4.0.3",
+ "lodash": "^4.17.21",
"next": "15.1.0",
"next-sitemap": "^4.2.3",
"node-fetch": "2.7.0",
@@ -40,6 +45,7 @@
"react": "19.0.0",
"react-big-calendar": "^1.17.0",
"react-dom": "19.0.0",
+ "react-hotkeys-hook": "^4.6.1",
"react-icons": "^5.0.1",
"react-markdown": "^9.0.1",
"rehype-raw": "^7.0.0",
@@ -59,6 +65,7 @@
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
+ "@types/lodash": "^4.17.14",
"@types/mdx": "^2.0.10",
"@types/node": "^20.9.1",
"@types/node-fetch": "^2.6.12",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1308610..e8f7923 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -20,6 +20,18 @@ importers:
'@auth0/nextjs-auth0':
specifier: ^3.5.0
version: 3.5.0(next@15.1.0(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
+ '@emotion/react':
+ specifier: ^11.14.0
+ version: 11.14.0(@types/react@19.0.1)(react@19.0.0)
+ '@emotion/styled':
+ specifier: ^11.14.0
+ version: 11.14.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react@19.0.0)
+ '@fontsource/roboto':
+ specifier: ^5.1.1
+ version: 5.1.1
+ '@mui/material':
+ specifier: ^6.4.0
+ version: 6.4.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@neondatabase/serverless':
specifier: ^0.10.4
version: 0.10.4
@@ -50,6 +62,9 @@ importers:
gray-matter:
specifier: ^4.0.3
version: 4.0.3
+ lodash:
+ specifier: ^4.17.21
+ version: 4.17.21
next:
specifier: 15.1.0
version: 15.1.0(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -74,6 +89,9 @@ importers:
react-dom:
specifier: 19.0.0
version: 19.0.0(react@19.0.0)
+ react-hotkeys-hook:
+ specifier: ^4.6.1
+ version: 4.6.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react-icons:
specifier: ^5.0.1
version: 5.4.0(react@19.0.0)
@@ -126,6 +144,9 @@ importers:
'@testing-library/user-event':
specifier: ^14.5.2
version: 14.5.2(@testing-library/dom@10.4.0)
+ '@types/lodash':
+ specifier: ^4.17.14
+ version: 4.17.14
'@types/mdx':
specifier: ^2.0.10
version: 2.0.13
@@ -797,6 +818,60 @@ packages:
'@emnapi/runtime@1.3.1':
resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
+ '@emotion/babel-plugin@11.13.5':
+ resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==}
+
+ '@emotion/cache@11.14.0':
+ resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==}
+
+ '@emotion/hash@0.9.2':
+ resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==}
+
+ '@emotion/is-prop-valid@1.3.1':
+ resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==}
+
+ '@emotion/memoize@0.9.0':
+ resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==}
+
+ '@emotion/react@11.14.0':
+ resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: '>=16.8.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@emotion/serialize@1.3.3':
+ resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==}
+
+ '@emotion/sheet@1.4.0':
+ resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==}
+
+ '@emotion/styled@11.14.0':
+ resolution: {integrity: sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==}
+ peerDependencies:
+ '@emotion/react': ^11.0.0-rc.0
+ '@types/react': '*'
+ react: '>=16.8.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@emotion/unitless@0.10.0':
+ resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==}
+
+ '@emotion/use-insertion-effect-with-fallbacks@1.2.0':
+ resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==}
+ peerDependencies:
+ react: '>=16.8.0'
+
+ '@emotion/utils@1.4.2':
+ resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==}
+
+ '@emotion/weak-memoize@0.4.0':
+ resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==}
+
'@esbuild-kit/core-utils@3.3.2':
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
deprecated: 'Merged into tsx: https://tsx.is'
@@ -1247,6 +1322,9 @@ packages:
resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@fontsource/roboto@5.1.1':
+ resolution: {integrity: sha512-XwVVXtERDQIM7HPUIbyDe0FP4SRovpjF7zMI8M7pbqFp3ahLJsJTd18h+E6pkar6UbV3btbwkKjYARr5M+SQow==}
+
'@hapi/hoek@9.3.0':
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
@@ -1413,6 +1491,86 @@ packages:
'@types/react': 19.0.1
react: '>=16'
+ '@mui/core-downloads-tracker@6.4.0':
+ resolution: {integrity: sha512-6u74wi+9zeNlukrCtYYET8Ed/n9AS27DiaXCZKAD3TRGFaqiyYSsQgN2disW83pI/cM1Q2lJY1JX4YfwvNtlNw==}
+
+ '@mui/material@6.4.0':
+ resolution: {integrity: sha512-hNIgwdM9U3DNmowZ8mU59oFmWoDKjc92FqQnQva3Pxh6xRKWtD2Ej7POUHMX8Dwr1OpcSUlT2+tEMeLb7WYsIg==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ '@emotion/react': ^11.5.0
+ '@emotion/styled': ^11.3.0
+ '@mui/material-pigment-css': ^6.4.0
+ '@types/react': 19.0.1
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@emotion/react':
+ optional: true
+ '@emotion/styled':
+ optional: true
+ '@mui/material-pigment-css':
+ optional: true
+ '@types/react':
+ optional: true
+
+ '@mui/private-theming@6.4.0':
+ resolution: {integrity: sha512-rNHci8MP6NOdEWAfZ/RBMO5Rhtp1T6fUDMSmingg9F1T6wiUeodIQ+NuTHh2/pMoUSeP9GdHdgMhMmfsXxOMuw==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ '@types/react': 19.0.1
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@mui/styled-engine@6.4.0':
+ resolution: {integrity: sha512-ek/ZrDujrger12P6o4luQIfRd2IziH7jQod2WMbLqGE03Iy0zUwYmckRTVhRQTLPNccpD8KXGcALJF+uaUQlbg==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ '@emotion/react': ^11.4.1
+ '@emotion/styled': ^11.3.0
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@emotion/react':
+ optional: true
+ '@emotion/styled':
+ optional: true
+
+ '@mui/system@6.4.0':
+ resolution: {integrity: sha512-wTDyfRlaZCo2sW2IuOsrjeE5dl0Usrs6J7DxE3GwNCVFqS5wMplM2YeNiV3DO7s53RfCqbho+gJY6xaB9KThUA==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ '@emotion/react': ^11.5.0
+ '@emotion/styled': ^11.3.0
+ '@types/react': 19.0.1
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@emotion/react':
+ optional: true
+ '@emotion/styled':
+ optional: true
+ '@types/react':
+ optional: true
+
+ '@mui/types@7.2.21':
+ resolution: {integrity: sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==}
+ peerDependencies:
+ '@types/react': 19.0.1
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@mui/utils@6.4.0':
+ resolution: {integrity: sha512-woOTATWNsTNR3YBh2Ixkj3l5RaxSiGoC9G8gOpYoFw1mZM77LWJeuMHFax7iIW4ahK0Cr35TF9DKtrafJmOmNQ==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ '@types/react': 19.0.1
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
'@neondatabase/serverless@0.10.4':
resolution: {integrity: sha512-2nZuh3VUO9voBauuh+IGYRhGU/MskWHt1IuZvHcJw6GLjDgtqj/KViKo7SIrLdGLdot7vFbiRRw+BgEy3wT9HA==}
@@ -2068,6 +2226,9 @@ packages:
'@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
+ '@types/lodash@4.17.14':
+ resolution: {integrity: sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==}
+
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -2106,6 +2267,11 @@ packages:
peerDependencies:
'@types/react': 19.0.1
+ '@types/react-transition-group@4.4.12':
+ resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==}
+ peerDependencies:
+ '@types/react': 19.0.1
+
'@types/react@19.0.1':
resolution: {integrity: sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ==}
@@ -2494,6 +2660,10 @@ packages:
'@babel/core': ^7.12.0
webpack: '>=5'
+ babel-plugin-macros@3.1.0:
+ resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
+ engines: {node: '>=10', npm: '>=6'}
+
babel-plugin-polyfill-corejs2@0.4.12:
resolution: {integrity: sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==}
peerDependencies:
@@ -2702,6 +2872,10 @@ packages:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
engines: {node: '>=6'}
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -3430,6 +3604,9 @@ packages:
resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==}
engines: {node: '>=14.16'}
+ find-root@1.1.0:
+ resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
+
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
@@ -3638,6 +3815,9 @@ packages:
hmac-drbg@1.0.1:
resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
+ hoist-non-react-statics@3.3.2:
+ resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
+
html-encoding-sniffer@4.0.0:
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
engines: {node: '>=18'}
@@ -4847,6 +5027,12 @@ packages:
peerDependencies:
react: ^19.0.0
+ react-hotkeys-hook@4.6.1:
+ resolution: {integrity: sha512-XlZpbKUj9tkfgPgT9gA+1p7Ey6vFIZHttUjPqpTdyT5nqQ8mHL7elxvSbaC+dpSiHUSmr21Ya1mDxBZG3aje4Q==}
+ peerDependencies:
+ react: '>=16.8.1'
+ react-dom: '>=16.8.1'
+
react-icons@5.4.0:
resolution: {integrity: sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==}
peerDependencies:
@@ -4858,6 +5044,9 @@ packages:
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+ react-is@19.0.0:
+ resolution: {integrity: sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==}
+
react-lifecycles-compat@3.0.4:
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
@@ -4877,6 +5066,12 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
+ react-transition-group@4.4.5:
+ resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
+ peerDependencies:
+ react: '>=16.6.0'
+ react-dom: '>=16.6.0'
+
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@@ -5148,6 +5343,10 @@ packages:
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+ source-map@0.5.7:
+ resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
+ engines: {node: '>=0.10.0'}
+
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
@@ -5282,6 +5481,9 @@ packages:
babel-plugin-macros:
optional: true
+ stylis@4.2.0:
+ resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
+
sucrase@3.35.0:
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -6591,6 +6793,89 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@emotion/babel-plugin@11.13.5':
+ dependencies:
+ '@babel/helper-module-imports': 7.25.9
+ '@babel/runtime': 7.26.0
+ '@emotion/hash': 0.9.2
+ '@emotion/memoize': 0.9.0
+ '@emotion/serialize': 1.3.3
+ babel-plugin-macros: 3.1.0
+ convert-source-map: 1.9.0
+ escape-string-regexp: 4.0.0
+ find-root: 1.1.0
+ source-map: 0.5.7
+ stylis: 4.2.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@emotion/cache@11.14.0':
+ dependencies:
+ '@emotion/memoize': 0.9.0
+ '@emotion/sheet': 1.4.0
+ '@emotion/utils': 1.4.2
+ '@emotion/weak-memoize': 0.4.0
+ stylis: 4.2.0
+
+ '@emotion/hash@0.9.2': {}
+
+ '@emotion/is-prop-valid@1.3.1':
+ dependencies:
+ '@emotion/memoize': 0.9.0
+
+ '@emotion/memoize@0.9.0': {}
+
+ '@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0)':
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@emotion/babel-plugin': 11.13.5
+ '@emotion/cache': 11.14.0
+ '@emotion/serialize': 1.3.3
+ '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.0.0)
+ '@emotion/utils': 1.4.2
+ '@emotion/weak-memoize': 0.4.0
+ hoist-non-react-statics: 3.3.2
+ react: 19.0.0
+ optionalDependencies:
+ '@types/react': 19.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@emotion/serialize@1.3.3':
+ dependencies:
+ '@emotion/hash': 0.9.2
+ '@emotion/memoize': 0.9.0
+ '@emotion/unitless': 0.10.0
+ '@emotion/utils': 1.4.2
+ csstype: 3.1.3
+
+ '@emotion/sheet@1.4.0': {}
+
+ '@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react@19.0.0)':
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@emotion/babel-plugin': 11.13.5
+ '@emotion/is-prop-valid': 1.3.1
+ '@emotion/react': 11.14.0(@types/react@19.0.1)(react@19.0.0)
+ '@emotion/serialize': 1.3.3
+ '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.0.0)
+ '@emotion/utils': 1.4.2
+ react: 19.0.0
+ optionalDependencies:
+ '@types/react': 19.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@emotion/unitless@0.10.0': {}
+
+ '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.0.0)':
+ dependencies:
+ react: 19.0.0
+
+ '@emotion/utils@1.4.2': {}
+
+ '@emotion/weak-memoize@0.4.0': {}
+
'@esbuild-kit/core-utils@3.3.2':
dependencies:
esbuild: 0.18.20
@@ -6846,6 +7131,8 @@ snapshots:
dependencies:
levn: 0.4.1
+ '@fontsource/roboto@5.1.1': {}
+
'@hapi/hoek@9.3.0': {}
'@hapi/topo@5.1.0':
@@ -7000,6 +7287,83 @@ snapshots:
react: 19.0.0
optional: true
+ '@mui/core-downloads-tracker@6.4.0': {}
+
+ '@mui/material@6.4.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@mui/core-downloads-tracker': 6.4.0
+ '@mui/system': 6.4.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react@19.0.0)
+ '@mui/types': 7.2.21(@types/react@19.0.1)
+ '@mui/utils': 6.4.0(@types/react@19.0.1)(react@19.0.0)
+ '@popperjs/core': 2.11.8
+ '@types/react-transition-group': 4.4.12(@types/react@19.0.1)
+ clsx: 2.1.1
+ csstype: 3.1.3
+ prop-types: 15.8.1
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ react-is: 19.0.0
+ react-transition-group: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ optionalDependencies:
+ '@emotion/react': 11.14.0(@types/react@19.0.1)(react@19.0.0)
+ '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react@19.0.0)
+ '@types/react': 19.0.1
+
+ '@mui/private-theming@6.4.0(@types/react@19.0.1)(react@19.0.0)':
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@mui/utils': 6.4.0(@types/react@19.0.1)(react@19.0.0)
+ prop-types: 15.8.1
+ react: 19.0.0
+ optionalDependencies:
+ '@types/react': 19.0.1
+
+ '@mui/styled-engine@6.4.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react@19.0.0))(react@19.0.0)':
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@emotion/cache': 11.14.0
+ '@emotion/serialize': 1.3.3
+ '@emotion/sheet': 1.4.0
+ csstype: 3.1.3
+ prop-types: 15.8.1
+ react: 19.0.0
+ optionalDependencies:
+ '@emotion/react': 11.14.0(@types/react@19.0.1)(react@19.0.0)
+ '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react@19.0.0)
+
+ '@mui/system@6.4.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react@19.0.0)':
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@mui/private-theming': 6.4.0(@types/react@19.0.1)(react@19.0.0)
+ '@mui/styled-engine': 6.4.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react@19.0.0))(react@19.0.0)
+ '@mui/types': 7.2.21(@types/react@19.0.1)
+ '@mui/utils': 6.4.0(@types/react@19.0.1)(react@19.0.0)
+ clsx: 2.1.1
+ csstype: 3.1.3
+ prop-types: 15.8.1
+ react: 19.0.0
+ optionalDependencies:
+ '@emotion/react': 11.14.0(@types/react@19.0.1)(react@19.0.0)
+ '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.0.1)(react@19.0.0))(@types/react@19.0.1)(react@19.0.0)
+ '@types/react': 19.0.1
+
+ '@mui/types@7.2.21(@types/react@19.0.1)':
+ optionalDependencies:
+ '@types/react': 19.0.1
+
+ '@mui/utils@6.4.0(@types/react@19.0.1)(react@19.0.0)':
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@mui/types': 7.2.21(@types/react@19.0.1)
+ '@types/prop-types': 15.7.14
+ clsx: 2.1.1
+ prop-types: 15.8.1
+ react: 19.0.0
+ react-is: 19.0.0
+ optionalDependencies:
+ '@types/react': 19.0.1
+
'@neondatabase/serverless@0.10.4':
dependencies:
'@types/pg': 8.11.6
@@ -7801,6 +8165,8 @@ snapshots:
'@types/json5@0.0.29': {}
+ '@types/lodash@4.17.14': {}
+
'@types/mdast@4.0.4':
dependencies:
'@types/unist': 3.0.3
@@ -7846,6 +8212,10 @@ snapshots:
dependencies:
'@types/react': 19.0.1
+ '@types/react-transition-group@4.4.12(@types/react@19.0.1)':
+ dependencies:
+ '@types/react': 19.0.1
+
'@types/react@19.0.1':
dependencies:
csstype: 3.1.3
@@ -8349,6 +8719,12 @@ snapshots:
schema-utils: 4.2.0
webpack: 5.97.1(esbuild@0.21.5)
+ babel-plugin-macros@3.1.0:
+ dependencies:
+ '@babel/runtime': 7.26.0
+ cosmiconfig: 7.1.0
+ resolve: 1.22.8
+
babel-plugin-polyfill-corejs2@0.4.12(@babel/core@7.26.0):
dependencies:
'@babel/compat-data': 7.26.3
@@ -8578,6 +8954,8 @@ snapshots:
clsx@1.2.1: {}
+ clsx@2.1.1: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -9437,6 +9815,8 @@ snapshots:
common-path-prefix: 3.0.0
pkg-dir: 7.0.0
+ find-root@1.1.0: {}
+
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
@@ -9734,6 +10114,10 @@ snapshots:
minimalistic-assert: 1.0.1
minimalistic-crypto-utils: 1.0.1
+ hoist-non-react-statics@3.3.2:
+ dependencies:
+ react-is: 16.13.1
+
html-encoding-sniffer@4.0.0:
dependencies:
whatwg-encoding: 3.1.1
@@ -11190,6 +11574,11 @@ snapshots:
react: 19.0.0
scheduler: 0.25.0
+ react-hotkeys-hook@4.6.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ dependencies:
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+
react-icons@5.4.0(react@19.0.0):
dependencies:
react: 19.0.0
@@ -11198,6 +11587,8 @@ snapshots:
react-is@17.0.2: {}
+ react-is@19.0.0: {}
+
react-lifecycles-compat@3.0.4: {}
react-markdown@9.0.1(@types/react@19.0.1)(react@19.0.0):
@@ -11232,6 +11623,15 @@ snapshots:
react-refresh@0.14.2: {}
+ react-transition-group@4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ dependencies:
+ '@babel/runtime': 7.26.0
+ dom-helpers: 5.2.1
+ loose-envify: 1.4.0
+ prop-types: 15.8.1
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+
react@18.3.1:
dependencies:
loose-envify: 1.4.0
@@ -11609,6 +12009,8 @@ snapshots:
buffer-from: 1.1.2
source-map: 0.6.1
+ source-map@0.5.7: {}
+
source-map@0.6.1: {}
source-map@0.7.4: {}
@@ -11756,6 +12158,8 @@ snapshots:
optionalDependencies:
'@babel/core': 7.26.0
+ stylis@4.2.0: {}
+
sucrase@3.35.0:
dependencies:
'@jridgewell/gen-mapping': 0.3.5