more brainstorming
This commit is contained in:
2210
client/package-lock.json
generated
2210
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,22 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@carbon/icons-react": "^11.53.0",
|
||||
"@internationalized/date": "^3.6.0",
|
||||
"@nextui-org/button": "^2.2.9",
|
||||
"@nextui-org/card": "^2.2.9",
|
||||
"@nextui-org/checkbox": "^2.3.8",
|
||||
"@nextui-org/date-picker": "^2.3.9",
|
||||
"@nextui-org/divider": "^2.2.5",
|
||||
"@nextui-org/input": "^2.4.8",
|
||||
"@nextui-org/modal": "^2.2.7",
|
||||
"@nextui-org/select": "^2.4.9",
|
||||
"@nextui-org/system": "^2.4.6",
|
||||
"@nextui-org/table": "^2.2.8",
|
||||
"@nextui-org/theme": "^2.4.5",
|
||||
"@nextui-org/tooltip": "^2.2.7",
|
||||
"@react-aria/i18n": "^3.12.4",
|
||||
"@react-stately/data": "^3.12.0",
|
||||
"framer-motion": "^11.15.0",
|
||||
"next": "15.1.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +1,115 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import { nextui } from "@nextui-org/theme";
|
||||
|
||||
const HIGHLIGHT = "#ff5053";
|
||||
const FOREGROUND = "#fef2ff";
|
||||
const ACCENT1 = "#b2aaff";
|
||||
const ACCENT2 = "#6a5fdb";
|
||||
const ACCENT3 = "#261a66";
|
||||
const ACCENT4 = "#29114c";
|
||||
const ACCENT5 = "#190b2f";
|
||||
const BACKGROUND = "#0f000a";
|
||||
|
||||
export default {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}"
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: "var(--background)",
|
||||
primary: "var(--primary)",
|
||||
highlight: "var(--highlight)",
|
||||
"accent-1": "var(--accent-1)",
|
||||
"accent-2": "var(--accent-2)",
|
||||
"accent-3": "var(--accent-3)",
|
||||
"accent-4": "var(--accent-4)",
|
||||
"accent-5": "var(--accent-5)"
|
||||
}
|
||||
highlight: HIGHLIGHT,
|
||||
foreground: {
|
||||
DEFAULT: FOREGROUND,
|
||||
"50": FOREGROUND,
|
||||
"100": "#fce8ff",
|
||||
"200": "#fad0fe",
|
||||
"300": "#f8abfc",
|
||||
"400": "#f579f9",
|
||||
"500": "#eb46ef",
|
||||
"600": "#d226d3",
|
||||
"700": "#af1cad",
|
||||
"800": "#8f198c",
|
||||
"900": "#751a70",
|
||||
"950": "#4e044a",
|
||||
},
|
||||
"accent-1": ACCENT1,
|
||||
"accent-2": ACCENT2,
|
||||
"accent-3": ACCENT3,
|
||||
"accent-4": ACCENT4,
|
||||
"accent-5": ACCENT5,
|
||||
background: BACKGROUND,
|
||||
},
|
||||
boxShadow: {
|
||||
border: `inset 0 0 0 2px ${ACCENT2}`,
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
"display-headline": ["pilowlava"],
|
||||
headline: ["spacegrotesk"],
|
||||
subheadline: ["uncut-sans"],
|
||||
body: ["uncut-sans"],
|
||||
numbers: ["space-mono"]
|
||||
}
|
||||
numbers: ["space-mono"],
|
||||
},
|
||||
},
|
||||
plugins: []
|
||||
darkMode: "class",
|
||||
plugins: [
|
||||
nextui({
|
||||
defaultTheme: "dark",
|
||||
defaultExtendTheme: "dark",
|
||||
themes: {
|
||||
dark: {
|
||||
colors: {
|
||||
// default: {
|
||||
// DEFAULT: ACCENT2,
|
||||
// },
|
||||
primary: {
|
||||
DEFAULT: ACCENT2,
|
||||
"50": "#39357a",
|
||||
"100": "#42399a",
|
||||
"200": "#5144be",
|
||||
"300": ACCENT2,
|
||||
"400": "#6f6ee6",
|
||||
"500": "#8b91ee",
|
||||
"600": "#acb7f5",
|
||||
"700": "#cbd3fa",
|
||||
"800": "#e2e7fd",
|
||||
"900": "#eff3fe",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: ACCENT3,
|
||||
"50": "#3b288a",
|
||||
"100": "#462fa8",
|
||||
"200": "#5538c9",
|
||||
"300": "#634add",
|
||||
"400": "#776ae8",
|
||||
"500": "#9a95f0",
|
||||
"600": "#bdbcf6",
|
||||
"700": "#dadbfa",
|
||||
"800": "#ebebfc",
|
||||
"900": "#f4f4fe",
|
||||
},
|
||||
// background: {
|
||||
// DEFAULT: BACKGROUND,
|
||||
// },
|
||||
danger: {
|
||||
DEFAULT: HIGHLIGHT,
|
||||
"50": "#fff1f1",
|
||||
"100": "#ffe1e2",
|
||||
"200": "#ffc7c8",
|
||||
"300": "#ffa0a2",
|
||||
"400": HIGHLIGHT,
|
||||
"500": "#f83b3e",
|
||||
"600": "#e51d20",
|
||||
"700": "#c11417",
|
||||
"800": "#a01416",
|
||||
"900": "#84181a",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
} satisfies Config;
|
||||
|
||||
Reference in New Issue
Block a user