changed names of database columns

This commit is contained in:
z1glr
2025-01-23 10:12:12 +00:00
parent 7265a4e36a
commit c752bc6c14
36 changed files with 604 additions and 605 deletions

View File

@@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@@ -1,17 +1,19 @@
"use client";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { Task } from "./lib";
import { Availability } from "./app/admin/(availabilities)/AvailabilityEditor";
export interface EventData {
id: number;
export interface BaseEvent {
eventID: number;
date: string;
tasks: TaskAssignment[];
description: string;
}
export type EventData = BaseEvent & {
tasks: TaskAssignment[];
};
interface TaskAssignment {
taskID: number;
taskName: string;
@@ -40,28 +42,27 @@ const initialState = {
};
const zustand = create<Zustand>()(
persist(
(set, get) => ({
...initialState,
reset: (newZustand) => {
console.debug("reset");
set({
...initialState,
...newZustand,
});
},
patch: (patch) => set({ ...get(), ...patch }),
}),
{
name: "golunteer-storage",
partialize: (state) =>
Object.fromEntries(
Object.entries(state).filter(([key]) =>
["user", "tasksList", "tasksMap"].includes(key),
),
),
// persist(
(set, get) => ({
...initialState,
reset: (newZustand) => {
set({
...initialState,
...newZustand,
});
},
),
patch: (patch) => set({ ...get(), ...patch }),
}),
// {
// name: "golunteer-storage",
// partialize: (state) =>
// Object.fromEntries(
// Object.entries(state).filter(([key]) =>
// ["user", "tasksList", "tasksMap"].includes(key),
// ),
// ),
// },
// ),
);
export default zustand;

View File

@@ -30,10 +30,7 @@ export default function Header({ sites }: { sites: SiteLink[] }) {
useEffect(() => {
(async () => {
const result = await apiCall<{ pendingEvents: number }>(
"GET",
"events/user/pending",
);
const result = await apiCall<number>("GET", "events/user/pending/count");
if (result.ok) {
setPendingEvents(await result.json());

View File

@@ -0,0 +1,29 @@
"use client";
import { apiCall } from "@/lib";
import { EventData } from "@/Zustand";
import { useAsyncList } from "@react-stately/data";
export default function MyEvents() {
const events = useAsyncList({
async load() {
const result = await apiCall<EventData[]>("GET", "events/user/assigned");
if (result.ok) {
return {
items: await result.json(),
};
} else {
return {
items: [],
};
}
},
});
return (
<div>
<h2>{events.items.map((e) => e.date)}</h2>
</div>
);
}

View File

@@ -1,33 +1,17 @@
"use client";
import { Add } from "@carbon/icons-react";
import { useState } from "react";
import AddEvent from "../components/Event/AddEvent";
import { Button } from "@heroui/react";
export default function EventVolunteer() {
const [showAddItemDialogue, setShowAddItemDialogue] = useState(false);
import MyEvents from "./MyEvents";
import PengingEvents from "./PendingEvents";
export default function Overview() {
return (
<div className="relative flex-1">
<h2 className="mb-4 text-center text-4xl">Overview</h2>
<div className="flex flex-wrap justify-center gap-4"></div>
<Button
color="primary"
isIconOnly
radius="full"
className="absolute bottom-0 right-0"
onPress={() => setShowAddItemDialogue(true)}
>
<Add size={32} />
</Button>
<AddEvent
className="border-2 border-accent-3"
isOpen={showAddItemDialogue}
onOpenChange={setShowAddItemDialogue}
/>
<h1 className="mb-4 text-center text-4xl">My Events</h1>
<MyEvents />
<h1 className="mb-4 text-center text-4xl">
events that I don't have entered an availability yet
</h1>
<PengingEvents />
</div>
);
}

View File

@@ -0,0 +1,77 @@
"use client";
import AvailabilityChip from "@/components/AvailabilityChip";
import Event from "@/components/Event/Event";
import { apiCall, getAvailabilities } from "@/lib";
import { BaseEvent } from "@/Zustand";
import { Select, SelectItem } from "@heroui/react";
import { useAsyncList } from "@react-stately/data";
type EventAvailability = BaseEvent & {
availability: number;
};
export default function PengingEvents() {
// get the events the user hasn't yet inserted his availability for
const events = useAsyncList({
async load() {
const result = await apiCall<EventAvailability[]>(
"GET",
"events/user/pending",
);
if (result.ok) {
return {
items: await result.json(),
};
} else {
return {
items: [],
};
}
},
});
// the individual, selectable availabilities
const availabilities = useAsyncList({
async load() {
return {
items: (await getAvailabilities()).filter((a) => a.enabled),
};
},
});
return (
<div className="flex justify-center gap-4">
{events.items.map((e) => (
<Event key={e.eventID} event={e}>
<Select
items={availabilities.items}
label="Availability"
variant="bordered"
className="mt-auto"
isMultiline
renderValue={(availability) => (
<div>
{availability.map((a) =>
!!a.data ? (
<AvailabilityChip key={a.key} availability={a.data} />
) : null,
)}
</div>
)}
>
{(availability) => (
<SelectItem
key={availability.availabilityID}
textValue={availability.availabilityName}
>
<AvailabilityChip availability={availability} />
</SelectItem>
)}
</Select>
</Event>
))}
</div>
);
}

View File

@@ -40,7 +40,7 @@ export default function Availabilities() {
switch (sortDescriptor.column) {
case "text":
cmp = a.name.localeCompare(b.name);
cmp = a.availabilityName.localeCompare(b.availabilityName);
break;
case "enabled":
if (a.enabled && !b.enabled) {
@@ -78,9 +78,11 @@ export default function Availabilities() {
availabilities.reload();
}
async function sendDeleteAvailability(id: number | undefined) {
if (id !== undefined) {
const result = await apiCall("DELETE", "availabilities", { id });
async function sendDeleteAvailability(availabilityID: number | undefined) {
if (availabilityID !== undefined) {
const result = await apiCall("DELETE", "availabilities", {
availabilityID,
});
if (result.ok) {
reload();
@@ -133,7 +135,7 @@ export default function Availabilities() {
</TableHeader>
<TableBody items={availabilities.items}>
{(availability) => (
<TableRow key={availability.name}>
<TableRow key={availability.availabilityName}>
<TableCell>
<AvailabilityChip availability={availability} />
</TableCell>
@@ -190,7 +192,9 @@ export default function Availabilities() {
!isOpen ? setDeleteAvailability(undefined) : null
}
itemName="Availability"
onDelete={() => sendDeleteAvailability(deleteAvailability?.id)}
onDelete={() =>
sendDeleteAvailability(deleteAvailability?.availabilityID)
}
>
{!!deleteAvailability ? (
<>

View File

@@ -1,4 +1,5 @@
import ColorSelector from "@/components/Colorselector";
import { AllString } from "@/lib";
import {
Checkbox,
Form,
@@ -12,9 +13,9 @@ import {
import React, { FormEvent, useEffect, useState } from "react";
export interface Availability {
name: string;
availabilityName: string;
color: string;
id: number | undefined;
availabilityID: number | undefined;
enabled: boolean;
}
@@ -26,7 +27,7 @@ export default function AvailabilityEditor(props: {
onOpenChange?: (isOpen: boolean) => void;
onSubmit?: (e: Availability) => void;
}) {
const [name, setName] = useState(props.value?.name ?? "");
const [name, setName] = useState(props.value?.availabilityName ?? "");
const [color, setColor] = useState(props.value?.color ?? "Red");
const [enabled, setEnabled] = useState(props.value?.enabled ?? true);
@@ -40,15 +41,13 @@ export default function AvailabilityEditor(props: {
}, [props.isOpen]);
function submit(e: FormEvent<HTMLFormElement>) {
const formData = Object.fromEntries(new FormData(e.currentTarget)) as {
name: string;
color: string;
enabled: string;
};
const formData = Object.fromEntries(
new FormData(e.currentTarget),
) as AllString<Exclude<Availability, "availabilityID">>;
props.onSubmit?.({
...formData,
id: props.value?.id,
availabilityID: props.value?.availabilityID,
enabled: formData.enabled == "true",
});
}
@@ -77,7 +76,7 @@ export default function AvailabilityEditor(props: {
<Input
value={name}
onValueChange={setName}
name="name"
name="availabilityName"
label="Name"
isRequired
variant="bordered"

View File

@@ -21,7 +21,7 @@ export default function EditAvailability(props: {
return (
<AvailabilityEditor
key={props.value?.id}
key={props.value?.availabilityID}
header={
<>
Edit Availability{" "}

View File

@@ -20,12 +20,12 @@ export default function EditTask(props: {
return (
<TaskEditor
key={props.value?.name}
key={props.value?.taskName}
header={
<>
Edit Task{" "}
<span className="font-numbers font-normal italic">
&quot;{props.value?.name}&quot;
&quot;{props.value?.taskName}&quot;
</span>
</>
}

View File

@@ -1,4 +1,4 @@
import { Task } from "@/lib";
import { AllString, Task } from "@/lib";
import {
Checkbox,
Form,
@@ -19,7 +19,7 @@ export default function TaskEditor(props: {
onOpenChange?: (isOpen: boolean) => void;
onSubmit?: (e: Task) => void;
}) {
const [name, setName] = useState(props.value?.name ?? "");
const [name, setName] = useState(props.value?.taskName ?? "");
const [enabled, setEnabled] = useState(props.value?.enabled ?? true);
// clear the inputs on closing
@@ -31,14 +31,13 @@ export default function TaskEditor(props: {
}, [props.isOpen]);
function submit(e: FormEvent<HTMLFormElement>) {
const formData = Object.fromEntries(new FormData(e.currentTarget)) as {
name: string;
enabled: string;
};
const formData = Object.fromEntries(
new FormData(e.currentTarget),
) as AllString<Exclude<Task, "taskID">>;
props.onSubmit?.({
...formData,
id: props.value?.id,
taskID: props.value?.taskID,
enabled: formData.enabled == "true",
});
}
@@ -68,7 +67,7 @@ export default function TaskEditor(props: {
<Input
value={name}
onValueChange={setName}
name="name"
name="taskName"
label="Name"
isRequired
variant="bordered"

View File

@@ -47,7 +47,7 @@ export default function Tasks() {
switch (sortDescriptor.column) {
case "text":
cmp = a.name.localeCompare(b.name);
cmp = a.taskName.localeCompare(b.taskName);
break;
case "enabled":
if (a.enabled && !b.enabled) {
@@ -76,9 +76,9 @@ export default function Tasks() {
tasks.reload();
}
async function sendDeleteTask(id: number | undefined) {
if (id !== undefined) {
const result = await apiCall("DELETE", "tasks", { id });
async function sendDeleteTask(taskID: number | undefined) {
if (taskID !== undefined) {
const result = await apiCall("DELETE", "tasks", { taskID });
if (result.ok) {
tasks.reload();
@@ -130,8 +130,8 @@ export default function Tasks() {
</TableHeader>
<TableBody items={tasks.items}>
{(task) => (
<TableRow key={task.id}>
<TableCell>{task.name}</TableCell>
<TableRow key={task.taskID}>
<TableCell>{task.taskName}</TableCell>
<TableCell>
<Checkbox isSelected={task.enabled} />
</TableCell>
@@ -181,13 +181,13 @@ export default function Tasks() {
isOpen={!!deleteTask}
onOpenChange={(isOpen) => (!isOpen ? setDeleteTask(undefined) : null)}
itemName="Task"
onDelete={() => sendDeleteTask(deleteTask?.id)}
onDelete={() => sendDeleteTask(deleteTask?.taskID)}
>
{!!deleteTask ? (
<>
The task{" "}
<span className="font-numbers text-accent-1">
{deleteTask.name}
{deleteTask.taskName}
</span>{" "}
will be deleted.
</>

View File

@@ -1,4 +1,8 @@
import { classNames, validatePassword as validatePassword } from "@/lib";
import {
AllString,
classNames,
validatePassword as validatePassword,
} from "@/lib";
import zustand, { User, UserAddModify } from "@/Zustand";
import {
Checkbox,
@@ -29,11 +33,9 @@ export default function UserEditor(props: {
// update the user in the backend
async function submit(e: FormEvent<HTMLFormElement>) {
const formData = Object.fromEntries(new FormData(e.currentTarget)) as {
userName: string;
password: string;
admin: string;
};
const formData = Object.fromEntries(
new FormData(e.currentTarget),
) as AllString<UserAddModify>;
const data = {
...formData,

View File

@@ -62,8 +62,8 @@ export default function AdminPanel() {
...tasks
.filter((task) => task.enabled)
.map((task) => ({
label: task.name,
key: task.id ?? -1,
label: task.taskName,
key: task.taskID ?? -1,
align: "center",
})),
{ key: "actions", label: "Action", align: "center" },
@@ -116,7 +116,9 @@ export default function AdminPanel() {
// send a delete request to the backend and close the popup on success
async function sendDeleteEvent() {
if (deleteEvent !== undefined) {
const result = await apiCall("DELETE", "event", { id: deleteEvent.id });
const result = await apiCall("DELETE", "event", {
eventID: deleteEvent.eventID,
});
if (result.ok) {
// store the received events
@@ -262,7 +264,7 @@ export default function AdminPanel() {
</TableHeader>
<TableBody items={events.items} emptyContent={"No events scheduled"}>
{(event) => (
<TableRow key={event.id}>
<TableRow key={event.eventID}>
{(columnKey) => (
<TableCell>{getKeyValue(event, columnKey)}</TableCell>
)}

View File

@@ -15,13 +15,11 @@ export default function Events() {
const events = useAsyncList<EventData>({
async load() {
const result = await apiCall("GET", "events/assignments");
const result = await apiCall<EventData[]>("GET", "events/assignments");
if (result.ok) {
const data = await result.json();
console.debug(data);
return {
items: data,
};
@@ -62,6 +60,7 @@ export default function Events() {
className="border-2 border-accent-3"
isOpen={showAddItemDialogue}
onOpenChange={setShowAddItemDialogue}
onSuccess={events.reload}
/>
</>
) : null}

View File

@@ -1,5 +1,5 @@
import EventVolunteer from "./Overview";
import Overview from "./Overview";
export default function Home() {
return <EventVolunteer />;
return <Overview />;
}

View File

@@ -17,7 +17,7 @@ export default function AvailabilityChip({
}}
className={className}
>
{availability.name}
{availability.availabilityName}
</Chip>
);
}

View File

@@ -24,7 +24,7 @@ export default function EditEvent(props: {
return (
<EventEditor
value={props.value}
key={props.value?.id}
key={props.value?.eventID}
header="Edit Event"
isOpen={props.isOpen}
onOpenChange={props.onOpenChange}

View File

@@ -1,7 +1,7 @@
"use client";
import LocalDate from "../LocalDate";
import { EventData } from "@/Zustand";
import { BaseEvent } from "@/Zustand";
import { Card, CardBody, CardHeader, Divider } from "@heroui/react";
import React from "react";
@@ -9,7 +9,7 @@ export default function Event({
event,
children,
}: {
event: EventData;
event: BaseEvent;
children?: React.ReactNode;
}) {
return (

View File

@@ -18,10 +18,12 @@ import {
Spinner,
Textarea,
} from "@heroui/react";
import zustand, { EventData } from "@/Zustand";
import { EventData } from "@/Zustand";
import { useAsyncList } from "@react-stately/data";
import { getTasks } from "@/lib";
export interface EventSubmitData {
id: number;
eventID: number;
date: string;
description: string;
tasks: number[];
@@ -47,12 +49,19 @@ export default function EventEditor(props: {
const [eventTasks, setEventTasks] = useState<string[]>(
props.value?.tasks.map((k) => k.taskID.toString()) ?? [],
);
const tasks = zustand((state) => state.tasks);
const tasks = useAsyncList({
async load() {
return {
items: await getTasks(),
};
},
});
function onSubmit() {
if (!!props.onSubmit && !!date) {
props.onSubmit({
id: props.value?.id ?? -1,
eventID: props.value?.eventID ?? -1,
date: date.toAbsoluteString(),
description,
tasks: eventTasks.map((t) => parseInt(t)),
@@ -109,12 +118,12 @@ export default function EventEditor(props: {
}
>
{!!tasks ? (
tasks
tasks.items
?.filter((task) => task.enabled)
.map((task) => (
<div key={task.id}>
<Checkbox value={task.id?.toString()}>
{task.name}
<div key={task.taskID}>
<Checkbox value={task.taskID?.toString()}>
{task.taskName}
</Checkbox>
</div>
))

View File

@@ -2,30 +2,32 @@ import { DateFormatter as IntlDateFormatter } from "@internationalized/date";
import zustand from "./Zustand";
import { Availability } from "./app/admin/(availabilities)/AvailabilityEditor";
export type AllString<T> = { [K in keyof T]: string };
type QueryParams = Record<string, string | { toString(): string }>;
export type APICallResult<T extends object> = Response & {
export type APICallResult<T> = Omit<Response, "json"> & {
json: () => Promise<T>;
};
export async function apiCall<K extends object>(
export async function apiCall<K>(
method: "GET",
api: string,
query?: QueryParams,
): Promise<APICallResult<K>>;
export async function apiCall<K extends object>(
export async function apiCall<K>(
method: "POST" | "PATCH" | "PUT",
api: string,
query?: QueryParams,
body?: object,
): Promise<APICallResult<K>>;
export async function apiCall<K extends object>(
export async function apiCall<K>(
method: "DELETE",
api: string,
query?: QueryParams,
body?: object,
): Promise<APICallResult<K>>;
export async function apiCall<K extends object>(
export async function apiCall<K>(
method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE",
api: string,
query?: QueryParams,
@@ -96,8 +98,8 @@ export function validatePassword(password: string): string[] {
}
export interface Task {
id: number | undefined;
name: string;
taskID: number | undefined;
taskName: string;
enabled: boolean;
}
@@ -105,7 +107,7 @@ export async function getTask(name: string): Promise<Task | undefined> {
// get the tasks
const tasks = await getTasks();
return tasks.find((t) => t.name === name);
return tasks.find((t) => t.taskName === name);
}
export async function getTasks(): Promise<Task[]> {
@@ -136,7 +138,7 @@ export async function getAvailabilities(): Promise<Availability[]> {
if (!!state.availabilities) {
return state.availabilities;
} else {
const result = await apiCall<Task[]>("GET", "availabilities");
const result = await apiCall<Availability[]>("GET", "availabilities");
if (result.ok) {
const tasks = await result.json();