more brainstorming
This commit is contained in:
76
client/src/Zustand.ts
Normal file
76
client/src/Zustand.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
DateFormatter as IntlDateFormatter,
|
||||
parseZonedDateTime,
|
||||
ZonedDateTime,
|
||||
} from "@internationalized/date";
|
||||
import { create } from "zustand";
|
||||
|
||||
export type Task = string;
|
||||
|
||||
export const Tasks: Task[] = [
|
||||
"Audio",
|
||||
"Livestream",
|
||||
"Camera",
|
||||
"Light",
|
||||
"Stream Audio",
|
||||
];
|
||||
|
||||
export type Availability = string;
|
||||
|
||||
export const Availabilities: Availability[] = ["yes", "maybe", "no"];
|
||||
|
||||
export interface EventData {
|
||||
id: number;
|
||||
date: ZonedDateTime;
|
||||
tasks: Partial<Record<Task, string | undefined>>;
|
||||
volunteers: Partial<Record<string, Availability>>;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Zustand {
|
||||
events: EventData[];
|
||||
addEvent: (event: EventData) => void;
|
||||
}
|
||||
|
||||
const zustand = create<Zustand>()((set) => ({
|
||||
events: [
|
||||
{
|
||||
id: 0,
|
||||
// date: parseDateTime("2025-01-05T11:00[Europe/Berlin]").toString(),
|
||||
date: parseZonedDateTime("2025-01-05T11:00[Europe/Berlin]"),
|
||||
tasks: {
|
||||
Audio: "Mark",
|
||||
Livestream: undefined,
|
||||
"Stream Audio": undefined,
|
||||
},
|
||||
volunteers: { Mark: "yes", Simon: "maybe", Sophie: "no" },
|
||||
description: "neuer Prädikant",
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
date: parseZonedDateTime("2025-01-12T11:00[Europe/Berlin]"),
|
||||
tasks: {
|
||||
Audio: "Mark",
|
||||
Livestream: undefined,
|
||||
},
|
||||
volunteers: { Mark: "yes", Simon: "maybe" },
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
addEvent: (event: EventData) =>
|
||||
set((state) => ({ events: state.events.toSpliced(-1, 0, event) })),
|
||||
}));
|
||||
|
||||
export class DateFormatter {
|
||||
private formatter;
|
||||
|
||||
constructor(locale: string, options?: Intl.DateTimeFormatOptions) {
|
||||
this.formatter = new IntlDateFormatter(locale, options);
|
||||
}
|
||||
|
||||
format(dt: Date) {
|
||||
return this.formatter.format(dt);
|
||||
}
|
||||
}
|
||||
|
||||
export default zustand;
|
||||
1
client/src/app/ManageEvents.tsx
Normal file
1
client/src/app/ManageEvents.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export default function ManageEvents() {}
|
||||
46
client/src/app/Overview.tsx
Normal file
46
client/src/app/Overview.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { Add } from "@carbon/icons-react";
|
||||
import Event from "../components/Event/Event";
|
||||
import { useState } from "react";
|
||||
import AddEvent from "../components/Event/AddEvent";
|
||||
import zustand from "../Zustand";
|
||||
import { Button } from "@nextui-org/button";
|
||||
|
||||
export default function EventVolunteer() {
|
||||
const [showAddItemDialogue, setShowAddItemDialogue] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative flex-1 p-4">
|
||||
<h2 className="mb-4 text-center text-4xl">Overview</h2>
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
{zustand.getState().events.map((ee, ii) => (
|
||||
<Event
|
||||
key={ii}
|
||||
date={ee.date}
|
||||
description={ee.description}
|
||||
id={ee.id}
|
||||
tasks={ee.tasks}
|
||||
volunteers={ee.volunteers}
|
||||
/>
|
||||
))}
|
||||
</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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
253
client/src/app/admin/page.tsx
Normal file
253
client/src/app/admin/page.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
"use client";
|
||||
|
||||
import AddEvent from "@/components/Event/AddEvent";
|
||||
import LocalDate from "@/components/Event/LocalDate";
|
||||
import zustand, { Availability, EventData, Task, Tasks } from "@/Zustand";
|
||||
import { Add, Copy, Edit, TrashCan } from "@carbon/icons-react";
|
||||
import { Button, ButtonGroup } from "@nextui-org/button";
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from "@nextui-org/modal";
|
||||
import { Select, SelectItem } from "@nextui-org/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableColumn,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@nextui-org/table";
|
||||
import { Tooltip } from "@nextui-org/tooltip";
|
||||
import { useAsyncList } from "@react-stately/data";
|
||||
import React, { Key, useState } from "react";
|
||||
|
||||
export default function AdminPanel() {
|
||||
const tasks = [
|
||||
{ key: "date", label: "Date" },
|
||||
{ key: "description", label: "Description" },
|
||||
...Tasks.map((task) => ({ label: task, key: task })),
|
||||
{ key: "actions", label: "Action" },
|
||||
];
|
||||
|
||||
const list = useAsyncList({
|
||||
async load() {
|
||||
return {
|
||||
items: [...zustand.getState().events],
|
||||
};
|
||||
},
|
||||
async sort({ items, sortDescriptor }) {
|
||||
return {
|
||||
items: items.sort((a, b) => {
|
||||
let cmp = 0;
|
||||
|
||||
// if it is the date-column, convert to a date
|
||||
if (sortDescriptor.column === "date") {
|
||||
const first = a[sortDescriptor.column];
|
||||
const second = b[sortDescriptor.column];
|
||||
|
||||
cmp = first < second ? -1 : 1;
|
||||
}
|
||||
|
||||
if (sortDescriptor.direction === "descending") {
|
||||
cmp *= -1;
|
||||
}
|
||||
|
||||
return cmp;
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function getKeyValue(event: EventData, key: Key): React.ReactNode {
|
||||
switch (key) {
|
||||
case "date":
|
||||
return (
|
||||
<LocalDate
|
||||
options={{ dateStyle: "medium", timeStyle: "short" }}
|
||||
className="font-bold"
|
||||
>
|
||||
{event[key].toDate()}
|
||||
</LocalDate>
|
||||
);
|
||||
case "description":
|
||||
return <span className="italic">{event[key]}</span>;
|
||||
case "actions":
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<ButtonGroup isIconOnly variant="light" size="sm">
|
||||
<Button>
|
||||
<Tooltip content="Edit event">
|
||||
<Edit />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
<Button>
|
||||
<Tooltip content="Duplicate event">
|
||||
<Copy />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
<Button
|
||||
color="danger"
|
||||
onPress={() => {
|
||||
setActiveEvent(event);
|
||||
setShowDeleteConfirm(true);
|
||||
}}
|
||||
>
|
||||
<Tooltip content="Delete event">
|
||||
<TrashCan />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
function availability2Tailwind(availability?: Availability) {
|
||||
switch (availability) {
|
||||
case "yes":
|
||||
return "";
|
||||
default:
|
||||
return "italic";
|
||||
}
|
||||
}
|
||||
|
||||
function availability2Color(availability?: Availability) {
|
||||
switch (availability) {
|
||||
case "yes":
|
||||
return "default";
|
||||
case "maybe":
|
||||
return "warning";
|
||||
default:
|
||||
return "danger";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
variant="underlined"
|
||||
fullWidth
|
||||
selectedKeys={new Set([event.tasks[key as Task] ?? ""])}
|
||||
classNames={{
|
||||
popoverContent: "w-fit",
|
||||
value: "mr-6",
|
||||
label: "mr-6",
|
||||
}}
|
||||
className="[&_*]:overflow-visible"
|
||||
>
|
||||
{Object.entries(event.volunteers).map(
|
||||
([volunteer, availability]) => (
|
||||
<SelectItem
|
||||
key={volunteer}
|
||||
color={availability2Color(availability)}
|
||||
className={[
|
||||
"text-" + availability2Color(availability),
|
||||
availability2Tailwind(availability),
|
||||
].join(" ")}
|
||||
>
|
||||
{volunteer}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const [showAddEvent, setShowAddEvent] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [activeEvent, setActiveEvent] = useState(zustand.getState().events[0]);
|
||||
|
||||
const topContent = (
|
||||
<div>
|
||||
<Button
|
||||
color="primary"
|
||||
startContent={<Add size={32} />}
|
||||
onPress={() => setShowAddEvent(true)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col items-center">
|
||||
<h2 className="mb-4 text-center text-4xl">Event Managment</h2>
|
||||
|
||||
<Table
|
||||
aria-label="Table with all the events"
|
||||
shadow="none"
|
||||
topContent={topContent}
|
||||
topContentPlacement="outside"
|
||||
isHeaderSticky
|
||||
sortDescriptor={list.sortDescriptor}
|
||||
onSortChange={list.sort}
|
||||
classNames={{
|
||||
wrapper: "bg-accent-4",
|
||||
tr: "even:bg-accent-5 ",
|
||||
th: "font-subheadline text-xl text-accent-1 bg-transparent ",
|
||||
thead: "[&>tr]:first:!shadow-border",
|
||||
}}
|
||||
className="w-fit"
|
||||
>
|
||||
<TableHeader columns={tasks}>
|
||||
{(task) => (
|
||||
<TableColumn
|
||||
allowsSorting={task.key === "date"}
|
||||
key={task.key}
|
||||
className=""
|
||||
>
|
||||
{task.label}
|
||||
</TableColumn>
|
||||
)}
|
||||
</TableHeader>
|
||||
<TableBody items={list.items} emptyContent={"No events scheduled"}>
|
||||
{(event) => (
|
||||
<TableRow key={event.id}>
|
||||
{(columnKey) => (
|
||||
<TableCell>{getKeyValue(event, columnKey)}</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<AddEvent isOpen={showAddEvent} onOpenChange={setShowAddEvent} />
|
||||
|
||||
<Modal
|
||||
isOpen={showDeleteConfirm}
|
||||
onOpenChange={setShowDeleteConfirm}
|
||||
shadow={"none" as "sm"}
|
||||
backdrop="blur"
|
||||
className="bg-accent-5"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<h1 className="text-2xl">Confirm event deletion</h1>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
The event{" "}
|
||||
<span className="font-numbers text-accent-1">
|
||||
<LocalDate options={{ dateStyle: "long", timeStyle: "short" }}>
|
||||
{activeEvent.date.toDate()}
|
||||
</LocalDate>
|
||||
</span>{" "}
|
||||
will be deleted.
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button startContent={<TrashCan />} color="danger">
|
||||
Delete event
|
||||
</Button>
|
||||
<Button
|
||||
variant="bordered"
|
||||
onPress={() => setShowDeleteConfirm(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import React, { MouseEventHandler } from "react";
|
||||
|
||||
export default function Button(props: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
onClick={props.onClick}
|
||||
className={`${props.className ?? ""} inline-block cursor-pointer rounded-full bg-accent-2 p-2`}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { CheckboxChecked, Checkbox } from "@carbon/icons-react";
|
||||
import { MouseEventHandler } from "react";
|
||||
|
||||
export default function CheckBox(props: {
|
||||
state: boolean;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
}) {
|
||||
return (
|
||||
<div onClick={props.onClick} className="inline-block cursor-pointer">
|
||||
{props.state ? <CheckboxChecked /> : <Checkbox />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import CheckBox from "../CheckBox";
|
||||
import { AddLarge } from "@carbon/icons-react";
|
||||
import Button from "../Button";
|
||||
import zustand, { EventData, ISODate, Task, Tasks } from "../Zustand";
|
||||
|
||||
interface state {
|
||||
date: string;
|
||||
description: string;
|
||||
tasks: Task[];
|
||||
}
|
||||
|
||||
export default function AddEvent(props: {
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [state, setState] = useState<state>({
|
||||
date: ISODate(new Date()),
|
||||
description: "",
|
||||
tasks: [],
|
||||
});
|
||||
|
||||
function toggleTask(task: Task) {
|
||||
const new_tasks = state.tasks.slice();
|
||||
|
||||
const index = new_tasks.indexOf(task);
|
||||
|
||||
if (index != -1) {
|
||||
new_tasks.splice(index, 1);
|
||||
} else {
|
||||
new_tasks.push(task);
|
||||
}
|
||||
|
||||
setState({
|
||||
...state,
|
||||
tasks: new_tasks,
|
||||
});
|
||||
}
|
||||
|
||||
function addEvent() {
|
||||
const eventData: EventData = {
|
||||
date: state.date,
|
||||
description: state.description,
|
||||
id: zustand.getState().events.slice(-1)[0].id + 1,
|
||||
tasks: {},
|
||||
};
|
||||
|
||||
// add all the tasks
|
||||
state.tasks.forEach((task) => {
|
||||
eventData.tasks[task] = undefined;
|
||||
});
|
||||
|
||||
zustand.getState().addEvent(eventData);
|
||||
|
||||
props.onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${props.className ?? ""} flex w-64 flex-col gap-2 rounded-xl bg-accent-5 p-4`}
|
||||
>
|
||||
<h1 className="text-2xl">Add Event</h1>
|
||||
<input
|
||||
type="date"
|
||||
value={state.date}
|
||||
onChange={(e) => console.log(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Description"
|
||||
value={state.description}
|
||||
onChange={(e) => setState({ ...state, description: e.target.value })}
|
||||
/>
|
||||
{Tasks.map((task, ii) => (
|
||||
<div
|
||||
key={ii}
|
||||
onClick={() => toggleTask(task)}
|
||||
className="flex cursor-default items-center gap-2"
|
||||
>
|
||||
<CheckBox state={state.tasks.includes(task)} />
|
||||
{task}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
className="ml-auto flex w-fit items-center justify-center gap-2 pr-4"
|
||||
onClick={addEvent}
|
||||
>
|
||||
<AddLarge size={32} />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { EventData } from "../Zustand";
|
||||
|
||||
export default function Event(props: EventData) {
|
||||
return (
|
||||
<div
|
||||
key={props.id}
|
||||
className="flex w-64 flex-col gap-2 rounded-xl bg-accent-5 p-4"
|
||||
>
|
||||
<h3 className="bold mb-1 text-2xl">{props.date}</h3>
|
||||
<div>{props.description}</div>
|
||||
|
||||
<table>
|
||||
<caption>
|
||||
<h4>Task assignment</h4>
|
||||
</caption>
|
||||
<tbody>
|
||||
{Object.entries(props.tasks).map(([task, person], ii) => (
|
||||
<tr key={ii}>
|
||||
<th className="pr-4 text-left">{task}</th>
|
||||
<td>{person ?? <span className="text-primary">missing</span>}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { AddLarge, CloseLarge } from "@carbon/icons-react";
|
||||
import Event from "./Event/Event";
|
||||
import { useState } from "react";
|
||||
import AddEvent from "./Event/AddEvent";
|
||||
import Button from "./Button";
|
||||
import zustand from "./Zustand";
|
||||
|
||||
export default function EventVolunteer() {
|
||||
const [showAddItemDialogue, setShowAddItemDialogue] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative flex-1 p-4">
|
||||
<h2 className="mb-4 text-center text-4xl">Overview</h2>
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
{zustand.getState().events.map((ee) => Event(ee))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="absolute bottom-0 right-0 aspect-square"
|
||||
onClick={() => setShowAddItemDialogue(true)}
|
||||
>
|
||||
<AddLarge size={32} />
|
||||
</Button>
|
||||
|
||||
{showAddItemDialogue ? (
|
||||
<div
|
||||
className="absolute inset-0 flex flex-col items-center backdrop-blur"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowAddItemDialogue(false);
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<AddEvent
|
||||
className="border-2 border-accent-3"
|
||||
onClose={() => setShowAddItemDialogue(false)}
|
||||
/>
|
||||
<Button
|
||||
className="absolute right-2 top-2 aspect-square"
|
||||
onClick={() => setShowAddItemDialogue(false)}
|
||||
>
|
||||
<CloseLarge />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
export enum Task {
|
||||
Audio = "Audio",
|
||||
Livestream = "Livestream",
|
||||
Camera = "Camera",
|
||||
Light = "Light",
|
||||
StreamAudio = "Stream Audio",
|
||||
}
|
||||
|
||||
export type TaskKey = keyof typeof Task;
|
||||
export const Tasks = Object.values(Task) as Task[];
|
||||
|
||||
export interface EventData {
|
||||
id: number;
|
||||
date: string;
|
||||
tasks: Partial<Record<Task, string | undefined>>;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Zustand {
|
||||
events: EventData[];
|
||||
addEvent: (event: EventData) => void;
|
||||
}
|
||||
|
||||
const zustand = create<Zustand>()((set) => ({
|
||||
events: [
|
||||
{
|
||||
id: 0,
|
||||
date: "2025-01-05",
|
||||
tasks: {
|
||||
Audio: "Mark",
|
||||
Livestream: undefined,
|
||||
"Stream Audio": undefined,
|
||||
},
|
||||
description: "neuer Prädikant",
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
date: "2025-01-12",
|
||||
tasks: {
|
||||
Audio: "Mark",
|
||||
Livestream: undefined,
|
||||
},
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
addEvent: (event: EventData) =>
|
||||
set((state) => ({ events: state.events.toSpliced(-1, 0, event) })),
|
||||
}));
|
||||
|
||||
export function ISODate(dt: string | Date): string {
|
||||
if (typeof dt === "string") {
|
||||
dt = new Date(dt);
|
||||
}
|
||||
|
||||
const year = String(dt.getFullYear()).padStart(4, "0");
|
||||
const month = String(dt.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(dt.getDate()).padStart(2, "0");
|
||||
|
||||
const date = `${year}-${month}-${day}`;
|
||||
|
||||
console.debug(date);
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
export default zustand;
|
||||
@@ -31,17 +31,6 @@
|
||||
src: URL("/fonts/uncut-sans/Webfonts/UncutSans-Regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #ff5053;
|
||||
--highlight: #fef2ff;
|
||||
--accent-1: #b2aaff;
|
||||
--accent-2: #6a5fdb;
|
||||
--accent-3: #261a66;
|
||||
--accent-4: #29114c;
|
||||
--accent-5: #190b2f;
|
||||
--background: #0f000a;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
h1,
|
||||
h2,
|
||||
@@ -49,14 +38,10 @@
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply font-headline text-primary;
|
||||
@apply font-headline text-highlight;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply border-2 border-accent-1 bg-transparent;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply bg-background font-body text-highlight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { NextUIProvider } from "@nextui-org/system";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app"
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="antialiased">{children}</body>
|
||||
<html>
|
||||
<body className="bg-background text-foreground antialiased">
|
||||
<NextUIProvider>
|
||||
<div className="flex min-h-screen flex-col p-4">
|
||||
<header>
|
||||
<h1 className="text-center font-display-headline text-8xl">
|
||||
Volunteer Scheduler
|
||||
</h1>
|
||||
</header>
|
||||
<main className="flex min-h-full flex-1 flex-col">{children}</main>
|
||||
</div>
|
||||
</NextUIProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
3
client/src/app/me/page.tsx
Normal file
3
client/src/app/me/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function OverviewPersonal() {
|
||||
return <div>foobar</div>;
|
||||
}
|
||||
@@ -1,16 +1,5 @@
|
||||
import EventVolunteer from "./components/Overview";
|
||||
import EventVolunteer from "./Overview";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="p-4 min-h-screen flex flex-col">
|
||||
<header>
|
||||
<h1 className="text-center text-8xl font-display-headline">
|
||||
Volunteer schedluer
|
||||
</h1>
|
||||
</header>
|
||||
<main className="min-h-full flex-1 flex">
|
||||
<EventVolunteer />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
return <EventVolunteer />;
|
||||
}
|
||||
|
||||
106
client/src/components/Event/AddEvent.tsx
Normal file
106
client/src/components/Event/AddEvent.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useState } from "react";
|
||||
import { Add } from "@carbon/icons-react";
|
||||
import zustand, { EventData, Task, Tasks } from "../../Zustand";
|
||||
import { Button } from "@nextui-org/button";
|
||||
import { Checkbox, CheckboxGroup } from "@nextui-org/checkbox";
|
||||
import { DatePicker } from "@nextui-org/date-picker";
|
||||
import { getLocalTimeZone, now, ZonedDateTime } from "@internationalized/date";
|
||||
import { Textarea } from "@nextui-org/input";
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from "@nextui-org/modal";
|
||||
|
||||
interface state {
|
||||
date: ZonedDateTime;
|
||||
description: string;
|
||||
tasks: Task[];
|
||||
}
|
||||
|
||||
export default function AddEvent(props: {
|
||||
className?: string;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
}) {
|
||||
const [state, setState] = useState<state>({
|
||||
date: now(getLocalTimeZone()),
|
||||
description: "",
|
||||
tasks: [],
|
||||
});
|
||||
|
||||
function addEvent() {
|
||||
const eventData: EventData = {
|
||||
date: state.date.toString(),
|
||||
description: state.description,
|
||||
id: zustand.getState().events.slice(-1)[0].id + 1,
|
||||
tasks: {},
|
||||
volunteers: {},
|
||||
};
|
||||
|
||||
// add all the tasks
|
||||
state.tasks.forEach((task) => {
|
||||
eventData.tasks[task] = undefined;
|
||||
});
|
||||
|
||||
zustand.getState().addEvent(eventData);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={props.isOpen}
|
||||
shadow={"none" as "sm"} // somehow "none" isn't allowed
|
||||
onOpenChange={props.onOpenChange}
|
||||
backdrop="blur"
|
||||
classNames={{
|
||||
base: "bg-accent-5 ",
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<h1 className="text-2xl">Add Event</h1>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<DatePicker
|
||||
label="Event date"
|
||||
variant="bordered"
|
||||
hideTimeZone
|
||||
granularity="minute"
|
||||
value={state.date}
|
||||
onChange={(dt) => (!!dt ? setState({ ...state, date: dt }) : null)}
|
||||
/>
|
||||
<Textarea
|
||||
variant="bordered"
|
||||
placeholder="Description"
|
||||
value={state.description}
|
||||
onValueChange={(desc) => setState({ ...state, description: desc })}
|
||||
/>
|
||||
<CheckboxGroup
|
||||
value={state.tasks}
|
||||
onValueChange={(newTasks) =>
|
||||
setState({ ...state, tasks: newTasks })
|
||||
}
|
||||
>
|
||||
{Tasks.map((task, ii) => (
|
||||
<div key={ii}>
|
||||
<Checkbox value={task}>{task}</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
</CheckboxGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
color="primary"
|
||||
radius="full"
|
||||
startContent={<Add size={32} />}
|
||||
onPress={addEvent}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
50
client/src/components/Event/Event.tsx
Normal file
50
client/src/components/Event/Event.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Card, CardBody, CardHeader } from "@nextui-org/card";
|
||||
import { Divider } from "@nextui-org/divider";
|
||||
import LocalDate from "./LocalDate";
|
||||
import { EventData } from "@/Zustand";
|
||||
|
||||
export default function Event(props: EventData) {
|
||||
return (
|
||||
<Card
|
||||
classNames={{
|
||||
base: "bg-accent-4 w-64",
|
||||
body: "flex flex-col gap-4",
|
||||
}}
|
||||
shadow="none"
|
||||
>
|
||||
<CardHeader>
|
||||
<h3 className="bold mb-1 text-2xl">
|
||||
<LocalDate
|
||||
options={{
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
timeZone: "Europe/Berlin", // TODO: check with actual backend
|
||||
}}
|
||||
>
|
||||
{props.date.toDate()}
|
||||
</LocalDate>
|
||||
</h3>
|
||||
</CardHeader>
|
||||
<Divider />
|
||||
<CardBody>
|
||||
<div>{props.description}</div>
|
||||
|
||||
<table>
|
||||
<caption>
|
||||
<h4>Task assignment</h4>
|
||||
</caption>
|
||||
<tbody>
|
||||
{Object.entries(props.tasks).map(([task, person], ii) => (
|
||||
<tr key={ii}>
|
||||
<th className="pr-4 text-left">{task}</th>
|
||||
<td>
|
||||
{person ?? <span className="text-highlight">missing</span>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
16
client/src/components/Event/LocalDate.tsx
Normal file
16
client/src/components/Event/LocalDate.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
"use local";
|
||||
|
||||
import { DateFormatter } from "@/Zustand";
|
||||
import { useLocale } from "@react-aria/i18n";
|
||||
|
||||
export default function LocalDate(props: {
|
||||
children: Date;
|
||||
className?: string;
|
||||
options: Intl.DateTimeFormatOptions;
|
||||
}) {
|
||||
const formatter = new DateFormatter(useLocale().locale, props.options);
|
||||
|
||||
return (
|
||||
<span className={props.className}>{formatter.format(props.children)}</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user