Skip to content

Commit 607de3a

Browse files
authored
Merge pull request #243 from theMattCode/feature/#226-people-backend
Implement people backend closes #226
2 parents d9fcddc + f2a912c commit 607de3a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1671
-49
lines changed

.github/dependabot.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ updates:
55
schedule:
66
interval: "weekly"
77
groups:
8-
dependencies:
9-
dependency-types: "production"
10-
devDependencies:
11-
dependency-types: "development"
8+
dev-deps:
9+
dependency-type: "development"
10+
prod-deps:
11+
dependency-type: "production"

app/(cms)/cms.css

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,36 +54,58 @@ p > ul > li {
5454
@apply ml-6
5555
}
5656

57+
@container (max-width: 600px) {
58+
table td:not(:nth-child(1)):not(:nth-child(2)):not(:last-child),
59+
table th:not(:nth-child(1)):not(:nth-child(2)):not(:last-child) {
60+
display: none;
61+
}
62+
}
63+
64+
@container (max-width: 900px) {
65+
table td:not(:nth-child(1)):not(:nth-child(2)):not(:nth-child(3)):not(:last-child),
66+
table th:not(:nth-child(1)):not(:nth-child(2)):not(:nth-child(3)):not(:last-child) {
67+
display: none;
68+
}
69+
}
70+
5771
table {
5872
@apply w-full font-light overflow-scroll
5973
}
6074

6175
th {
62-
@apply border-0 px-3 py-2 first:rounded-tl-md last:rounded-tr-md text-left text-sm uppercase
76+
@apply border-0 p-2 first:rounded-tl-md last:rounded-tr-md text-left text-sm uppercase
6377
}
6478

6579
tr {
66-
@apply even:bg-gray-100 first:border-t border-0 border-gray-100
80+
@apply even:bg-gray-50 first:border-t border-0 border-gray-100
6781
}
6882

6983
thead > tr {
7084
@apply font-medium
7185
}
7286

7387
td {
74-
@apply border-0 border-b border-gray-300 px-3 py-2 font-normal
88+
@apply border-0 border-b border-gray-100 p-2 font-normal
7589
}
7690

7791
.card-base {
78-
@apply bg-white rounded-lg border border-neutral-200 flex flex-col
92+
@apply bg-white rounded-lg border border-neutral-200 flex flex-col h-full
7993
}
8094

81-
.card-content {
82-
@apply w-full p-4
95+
.card-header {
96+
@apply w-full px-4 pt-4 flex justify-between
8397
}
8498

8599
.card-title {
86-
@apply w-full p-4 text-lg font-medium text-gray-700
100+
@apply text-lg font-medium text-gray-700
101+
}
102+
103+
.card-toolbar {
104+
@apply flex justify-end gap-4 items-center
105+
}
106+
107+
.card-content {
108+
@apply w-full p-4 flex flex-col gap-4 @container
87109
}
88110

89111
.news-title-shadow {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { drizzle } from "#/lib/db/drizzle";
3+
import { people } from "#/lib/db/schema";
4+
import { revalidatePath } from "next/cache";
5+
6+
export async function POST(request: NextRequest) {
7+
const newPerson = await drizzle
8+
.insert(people)
9+
.values({ id: crypto.randomUUID(), firstName: "", lastName: "", phone: "", email: "", image: "" })
10+
.returning();
11+
revalidatePath("/cms/people");
12+
return NextResponse.json({ type: "success", person: { ...newPerson[0] } });
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { drizzle } from "#/lib/db/drizzle";
3+
4+
export async function GET(request: NextRequest) {
5+
const roles = await drizzle.query.roles.findMany();
6+
return NextResponse.json({ roles });
7+
}

app/(cms)/cms/api/people/route.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { drizzle } from "#/lib/db/drizzle";
3+
import { PersonWithRoles } from "#/lib/types/people";
4+
5+
export async function GET(request: NextRequest) {
6+
const people = await drizzle.query.people.findMany({
7+
with: {
8+
peopleToRoles: {
9+
with: {
10+
roles: true,
11+
},
12+
},
13+
},
14+
});
15+
return NextResponse.json<{ people: PersonWithRoles[] }>({ people });
16+
}

app/(cms)/cms/error.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"use client";
2+
3+
import { Alert, AlertTitle } from "@mui/material";
4+
5+
export default function Error({ error }: { error: Error & { digest?: string }; reset: () => void }) {
6+
return (
7+
<div className="container">
8+
<Alert severity="error">
9+
<AlertTitle>Es ist ein Fehler aufgetreten</AlertTitle>
10+
{error.message}
11+
</Alert>
12+
</div>
13+
);
14+
}

app/(cms)/cms/layout.tsx

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,43 @@
11
"use client";
22

3+
import "@fontsource/roboto/300.css";
4+
import "@fontsource/roboto/400.css";
5+
import "@fontsource/roboto/500.css";
6+
import "@fontsource/roboto/700.css";
37
import { PropsWithChildren, useState } from "react";
48
import { withPageAuthRequired } from "@auth0/nextjs-auth0/client";
59
import { Navigation } from "#/components/cms/navigation/Navigation";
610
import { Header } from "#/components/cms/header/Header";
11+
import { createTheme, ThemeProvider } from "@mui/material";
12+
13+
const theme = createTheme({
14+
palette: {
15+
primary: {
16+
light: "#3999d9",
17+
main: "#057DB1",
18+
dark: "#015d98",
19+
contrastText: "#fff",
20+
},
21+
},
22+
});
723

824
export default withPageAuthRequired(function CMSLayout({ children }: PropsWithChildren) {
925
const [navigationOpen, setNavigationOpen] = useState(false);
1026
return (
11-
<div className="@container w-screen h-screen flex bg-gray-50">
12-
<Navigation open={navigationOpen} onClose={() => setNavigationOpen(false)} />
13-
<div className="w-full h-full flex flex-col" onClick={navigationOpen ? () => setNavigationOpen(false) : () => {}}>
14-
<Header onOpenMenu={() => setNavigationOpen(true)} />
15-
<main className="w-full h-full overflow-y-auto p-4 @container">{children}</main>
27+
<ThemeProvider theme={theme}>
28+
<div className="@container w-screen h-screen flex bg-gray-50">
29+
<Navigation open={navigationOpen} onClose={() => setNavigationOpen(false)} />
1630
<div
17-
className={`transition-all z-30 @5xl:hidden ${navigationOpen ? "fixed top-0 left-0 w-full h-full bg-neutral-500/50" : ""}`}
18-
/>
31+
className="w-full h-full flex flex-col"
32+
onClick={navigationOpen ? () => setNavigationOpen(false) : () => {}}
33+
>
34+
<Header onOpenMenu={() => setNavigationOpen(true)} />
35+
<main className="w-full h-full overflow-y-auto p-4 @container">{children}</main>
36+
<div
37+
className={`transition-all z-30 @5xl:hidden ${navigationOpen ? "fixed top-0 left-0 w-full h-full bg-neutral-500/50" : ""}`}
38+
/>
39+
</div>
1940
</div>
20-
</div>
41+
</ThemeProvider>
2142
);
2243
});

app/(cms)/cms/loading.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { LinearProgress } from "@mui/material";
2+
3+
export default function Loading() {
4+
return (
5+
<div className="container">
6+
<LinearProgress />
7+
</div>
8+
);
9+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5+
import { PersonWithRoles } from "#/lib/types/people";
6+
import { CellContext, createColumnHelper, getCoreRowModel, TableOptions } from "@tanstack/table-core";
7+
import { Table } from "#/components/cms/table/Table";
8+
import { PhoneCell } from "#/components/cms/table/cell/Phone";
9+
import { MailCell } from "#/components/cms/table/cell/Mail";
10+
import { RolesCell } from "#/components/cms/table/cell/Roles";
11+
import { FaRegEdit } from "react-icons/fa";
12+
import { CardToolbar } from "#/components/cms/card/Card";
13+
import { debounce } from "lodash";
14+
import { LuSearch } from "react-icons/lu";
15+
import { MdOutlineDelete, MdOutlinePersonAdd } from "react-icons/md";
16+
import { Button, IconButton, LinearProgress } from "@mui/material";
17+
import { TextField } from "#/components/cms/input/TextField";
18+
import { deletePerson, readAllPeople } from "#/app/(cms)/cms/people/actions";
19+
import { useRouter } from "next/navigation";
20+
import { useHotkeys } from "react-hotkeys-hook";
21+
22+
const COLUMN_HELPER = createColumnHelper<PersonWithRoles>();
23+
24+
const COLUMN_FIRSTNAME = COLUMN_HELPER.accessor("firstName", {
25+
header: "Vorname",
26+
id: "firstName",
27+
});
28+
29+
const COLUMN_LASTNAME = COLUMN_HELPER.accessor("lastName", {
30+
header: "Nachname",
31+
id: "lastName",
32+
});
33+
34+
const COLUMN_EMAIL = COLUMN_HELPER.accessor("email", {
35+
header: "E-Mail",
36+
id: "email",
37+
cell: MailCell,
38+
});
39+
40+
const COLUMN_PHONE = COLUMN_HELPER.accessor("phone", {
41+
header: "Phone",
42+
id: "phone",
43+
cell: PhoneCell,
44+
});
45+
46+
const COLUMN_ROLES = COLUMN_HELPER.accessor((person) => person.peopleToRoles.map((p) => p.roles.name), {
47+
header: "Rollen",
48+
id: "roles",
49+
cell: RolesCell,
50+
});
51+
52+
export default function PeopleList() {
53+
const [people, setPeople] = useState<PersonWithRoles[] | undefined>(undefined);
54+
55+
useEffect(() => {
56+
async function fetchAllPeople() {
57+
const response = await fetch("/cms/api/people");
58+
const data = await response.json();
59+
setPeople(data.people);
60+
}
61+
fetchAllPeople();
62+
}, []);
63+
64+
const router = useRouter();
65+
66+
const [searchTerm, setSearchTerm] = useState<string | null>();
67+
const filteredPeople = useMemo(() => {
68+
if (people === undefined) return [];
69+
70+
if (!searchTerm || searchTerm.trim() === "") return people;
71+
72+
return people.filter((person) => {
73+
const firstName = person.firstName?.toLowerCase();
74+
const lastName = person.lastName?.toLowerCase();
75+
76+
if (firstName?.includes(searchTerm)) return true;
77+
if (lastName?.includes(searchTerm)) return true;
78+
if (`${firstName} ${lastName}`.includes(searchTerm)) return true;
79+
if (person.email?.toLowerCase().includes(searchTerm)) return true;
80+
return person.peopleToRoles.some((p) => p.roles.name?.toLowerCase().includes(searchTerm) ?? false);
81+
});
82+
}, [searchTerm, people]);
83+
84+
const options: TableOptions<PersonWithRoles> = useMemo(
85+
() => ({
86+
data: filteredPeople,
87+
columns: [
88+
COLUMN_LASTNAME,
89+
COLUMN_FIRSTNAME,
90+
COLUMN_EMAIL,
91+
COLUMN_PHONE,
92+
COLUMN_ROLES,
93+
COLUMN_HELPER.display({
94+
header: "",
95+
id: "actions",
96+
cell: (props) => (
97+
<Actions
98+
{...props}
99+
onDelete={(personId) => setPeople((prev) => prev?.filter((person) => person.id !== personId))}
100+
/>
101+
),
102+
}),
103+
],
104+
initialState: {
105+
sorting: [{ id: "lastName", desc: false }],
106+
},
107+
getRowId: (person) => person.id,
108+
getCoreRowModel: getCoreRowModel(),
109+
}),
110+
[filteredPeople],
111+
);
112+
113+
const onChange = useCallback(
114+
debounce((value: string | null) => setSearchTerm(value?.trim().toLowerCase()), 500),
115+
[],
116+
);
117+
118+
const onNewPerson = useCallback(async () => {
119+
router.push("/cms/people/add");
120+
}, [router]);
121+
useHotkeys("alt+n", onNewPerson);
122+
123+
const refSearch = useRef<HTMLDivElement>(null);
124+
const focusSearch = useCallback(() => {
125+
refSearch.current?.focus();
126+
}, []);
127+
useHotkeys("alt+s", focusSearch);
128+
129+
return (
130+
<>
131+
<CardToolbar>
132+
<TextField inputRef={refSearch} fullWidth={false} onChange={onChange} label="Suche" StartIcon={LuSearch} />
133+
<Button variant="contained" aria-label="Neue Person" startIcon={<MdOutlinePersonAdd />} href="/cms/people/add">
134+
Neue Person
135+
</Button>
136+
</CardToolbar>
137+
<Table options={options} loading={people === undefined} />
138+
</>
139+
);
140+
}
141+
142+
function Actions<TData extends { id: string }>({
143+
onDelete,
144+
...cellContext
145+
}: CellContext<TData, unknown> & { onDelete: (personId: string) => void }) {
146+
const router = useRouter();
147+
return (
148+
<div className="w-full flex place-content-end gap-2">
149+
<IconButton
150+
size="small"
151+
onClick={() => {
152+
router.push(`/cms/people/${cellContext.row.original.id}`);
153+
}}
154+
>
155+
<FaRegEdit />
156+
</IconButton>
157+
<IconButton
158+
size="small"
159+
onClick={async () => {
160+
await deletePerson(cellContext.row.original.id);
161+
onDelete(cellContext.row.original.id);
162+
}}
163+
>
164+
<MdOutlineDelete />
165+
</IconButton>
166+
</div>
167+
);
168+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Card, CardContent, CardHeader, CardTitle } from "#/components/cms/card/Card";
2+
import PeopleList from "#/app/(cms)/cms/people/PeopleList";
3+
4+
export async function PeopleListCard() {
5+
return (
6+
<Card>
7+
<CardHeader>
8+
<CardTitle>Personen</CardTitle>
9+
</CardHeader>
10+
<CardContent>
11+
<PeopleList />
12+
</CardContent>
13+
</Card>
14+
);
15+
}

0 commit comments

Comments
 (0)