started "real work"
This commit is contained in:
@@ -2,7 +2,8 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
// output: "export",
|
||||
output: "export",
|
||||
|
||||
rewrites: async () => ({
|
||||
beforeFiles: [],
|
||||
afterFiles: [],
|
||||
|
||||
17301
client/package-lock.json
generated
17301
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,52 +1,38 @@
|
||||
{
|
||||
"name": "client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"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/form": "^2.1.8",
|
||||
"@nextui-org/input": "^2.4.8",
|
||||
"@nextui-org/link": "^2.2.7",
|
||||
"@nextui-org/modal": "^2.2.7",
|
||||
"@nextui-org/radio": "^2.3.8",
|
||||
"@nextui-org/select": "^2.4.9",
|
||||
"@nextui-org/spinner": "^2.2.6",
|
||||
"@nextui-org/switch": "^2.2.8",
|
||||
"@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",
|
||||
"zustand": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.3",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
"name": "client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@carbon/icons-react": "^11.53.0",
|
||||
"@internationalized/date": "^3.6.0",
|
||||
"@nextui-org/react": "^2.6.11",
|
||||
"@nextui-org/system": "^2.4.6",
|
||||
"@nextui-org/theme": "^2.4.5",
|
||||
"@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",
|
||||
"zustand": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.3",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { DateFormatter as IntlDateFormatter } from "@internationalized/date";
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
export type Task = string;
|
||||
|
||||
@@ -19,43 +22,48 @@ export interface EventData {
|
||||
id: number;
|
||||
date: string;
|
||||
tasks: Partial<Record<Task, string | undefined>>;
|
||||
volunteers: Partial<Record<string, Availability>>;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Zustand {
|
||||
events: EventData[];
|
||||
addEvent: (event: EventData) => void;
|
||||
pendingEvents: number;
|
||||
user: {
|
||||
userName: string;
|
||||
admin: boolean;
|
||||
} | null;
|
||||
setEvents: (events: EventData[]) => void;
|
||||
reset: (zustand?: Partial<Zustand>) => void;
|
||||
setPendingEvents: (c: number) => void;
|
||||
}
|
||||
|
||||
const zustand = create<Zustand>()((set) => ({
|
||||
events: [
|
||||
const initialState = {
|
||||
events: [],
|
||||
user: null,
|
||||
pendingEvents: 0,
|
||||
};
|
||||
|
||||
const zustand = create<Zustand>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...initialState,
|
||||
setEvents: (events) => set({ events }),
|
||||
reset: (newZustand) =>
|
||||
set({
|
||||
...initialState,
|
||||
...newZustand,
|
||||
}),
|
||||
setPendingEvents: (c) => set(() => ({ pendingEvents: c })),
|
||||
}),
|
||||
{
|
||||
id: 0,
|
||||
// date: parseDateTime("2025-01-05T11:00[Europe/Berlin]").toString(),
|
||||
date: "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",
|
||||
name: "golunteer-storage",
|
||||
partialize: (state) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(state).filter(([key]) => !["events"].includes(key)),
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
date: "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;
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { Divider } from "@nextui-org/divider";
|
||||
import { Link } from "@nextui-org/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React from "react";
|
||||
import { SiteLink } from "./layout";
|
||||
import { Divider, Link } from "@nextui-org/react";
|
||||
|
||||
export default function Footer({
|
||||
sites,
|
||||
}: {
|
||||
sites: { href: string; text: string }[];
|
||||
}) {
|
||||
export default function Footer({ sites }: { sites: SiteLink[] }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,124 @@
|
||||
import { Link } from "@nextui-org/link";
|
||||
"use client";
|
||||
|
||||
import { apiCall } from "@/lib";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { User } from "@carbon/icons-react";
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownTrigger,
|
||||
Link,
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
} from "@nextui-org/react";
|
||||
import zustand from "@/Zustand";
|
||||
import { SiteLink } from "./layout";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function Header({ sites }: { sites: SiteLink[] }) {
|
||||
const router = useRouter();
|
||||
const user = zustand((state) => state.user);
|
||||
const pendingEvents = zustand((state) => state.pendingEvents);
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
async function logout() {
|
||||
const result = await apiCall("GET", "logout");
|
||||
|
||||
// if the request was successfull, redirect to the login-page
|
||||
if (result.ok) {
|
||||
// clear the zustand
|
||||
zustand.getState().reset();
|
||||
|
||||
router.push("/login");
|
||||
}
|
||||
}
|
||||
|
||||
// get the pending events for the counter
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const result = await apiCall<{ pendingEvents: number }>(
|
||||
"GET",
|
||||
"events/user/pending",
|
||||
);
|
||||
|
||||
if (result.ok) {
|
||||
const resultJson = await result.json();
|
||||
zustand.getState().setPendingEvents(resultJson);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<Link href="/" className="text-center text-8xl">
|
||||
<h1 className="font-display-headline">Volunteer Scheduler</h1>
|
||||
</Link>
|
||||
<div>
|
||||
<Navbar maxWidth="full">
|
||||
<NavbarBrand onClick={() => router.push("/")}>
|
||||
<h1 className="font-display-headline text-xl">Golunteer</h1>
|
||||
</NavbarBrand>
|
||||
|
||||
{user !== null ? (
|
||||
<>
|
||||
<NavbarContent justify="center">
|
||||
{sites.map((s) =>
|
||||
// if the site is no admin-site or the user is an admin, render it
|
||||
!s.admin || user.admin ? (
|
||||
<NavbarItem key={s.href} isActive={pathname === s.href}>
|
||||
<Link
|
||||
href={s.href}
|
||||
color={pathname === s.href ? "primary" : "foreground"}
|
||||
>
|
||||
{s.text}
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
) : null,
|
||||
)}
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent justify="end">
|
||||
<Dropdown placement="bottom-end">
|
||||
<Badge
|
||||
content={pendingEvents}
|
||||
color="danger"
|
||||
aria-label={`${pendingEvents} notifications`}
|
||||
>
|
||||
<DropdownTrigger>
|
||||
<Avatar isBordered as="button" icon={<User size={32} />} />
|
||||
</DropdownTrigger>
|
||||
</Badge>
|
||||
<DropdownMenu variant="flat">
|
||||
<DropdownItem
|
||||
key="profile"
|
||||
className="h-14 gap-2"
|
||||
textValue={`signed in as ${user.userName}`}
|
||||
>
|
||||
<p>Signed in as</p>
|
||||
<p className="text-primary">{user.userName}</p>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="account"
|
||||
onPress={() => router.push("/account")}
|
||||
>
|
||||
Account
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="logout"
|
||||
color="danger"
|
||||
onPress={logout}
|
||||
className="text-danger"
|
||||
>
|
||||
Log Out
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</NavbarContent>
|
||||
</>
|
||||
) : null}
|
||||
</Navbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,49 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { apiCall } from "@/lib";
|
||||
import { Spinner } from "@nextui-org/spinner";
|
||||
import zustand from "@/Zustand";
|
||||
import { Spinner } from "@nextui-org/react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
enum AuthState {
|
||||
Loading,
|
||||
LoggedIn,
|
||||
LoginScreen,
|
||||
Unauthorized,
|
||||
LoggedIn,
|
||||
Loading,
|
||||
}
|
||||
|
||||
export default function Main({ children }: { children: React.ReactNode }) {
|
||||
const [status, setStatus] = useState(AuthState.Loading);
|
||||
const [auth, setAuth] = useState(AuthState.Loading);
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
if (pathname === "/login") {
|
||||
setStatus(AuthState.LoginScreen);
|
||||
} else {
|
||||
let loggedIn = false;
|
||||
|
||||
if (zustand.getState().user === null) {
|
||||
const welcomeResult = await apiCall<{
|
||||
userName: string;
|
||||
loggedIn: boolean;
|
||||
}>("GET", "welcome");
|
||||
|
||||
if (!welcomeResult.ok) {
|
||||
router.push("/login");
|
||||
} else {
|
||||
const response = await welcomeResult.json();
|
||||
if (welcomeResult.ok) {
|
||||
try {
|
||||
const response = await welcomeResult.json();
|
||||
|
||||
if (response.loggedIn) {
|
||||
setStatus(AuthState.LoggedIn);
|
||||
} else {
|
||||
setStatus(AuthState.Unauthorized);
|
||||
}
|
||||
if (response.userName !== undefined && response.userName !== "") {
|
||||
zustand.getState().reset({ user: response });
|
||||
|
||||
loggedIn = true;
|
||||
}
|
||||
} catch {}
|
||||
} else {
|
||||
}
|
||||
} else {
|
||||
loggedIn = true;
|
||||
}
|
||||
|
||||
if (pathname === "/login") {
|
||||
if (loggedIn) {
|
||||
router.push("/");
|
||||
} else {
|
||||
setAuth(AuthState.LoginScreen);
|
||||
}
|
||||
} else {
|
||||
if (loggedIn) {
|
||||
setAuth(AuthState.LoggedIn);
|
||||
} else {
|
||||
setAuth(AuthState.Unauthorized);
|
||||
router.push("/login");
|
||||
}
|
||||
}
|
||||
})();
|
||||
});
|
||||
}, [pathname, router]);
|
||||
|
||||
switch (status) {
|
||||
switch (auth) {
|
||||
case AuthState.Loading:
|
||||
return <Spinner label="Loading..." />;
|
||||
case AuthState.LoggedIn:
|
||||
|
||||
@@ -5,10 +5,10 @@ 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";
|
||||
import AssignmentTable from "@/components/Event/AssignmentTable";
|
||||
import { useAsyncList } from "@react-stately/data";
|
||||
import { apiCall } from "@/lib";
|
||||
import { Button } from "@nextui-org/react";
|
||||
|
||||
export default function EventVolunteer() {
|
||||
const [showAddItemDialogue, setShowAddItemDialogue] = useState(false);
|
||||
@@ -18,8 +18,6 @@ export default function EventVolunteer() {
|
||||
load: async () => {
|
||||
const data = await apiCall("GET", "events");
|
||||
|
||||
console.debug(await data.json());
|
||||
|
||||
return {
|
||||
items: [],
|
||||
};
|
||||
@@ -27,7 +25,7 @@ export default function EventVolunteer() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative flex-1 p-4">
|
||||
<div className="relative flex-1">
|
||||
<h2 className="mb-4 text-center text-4xl">Overview</h2>
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
{zustand.getState().events.map((ee) => (
|
||||
|
||||
@@ -4,24 +4,24 @@ import AddEvent from "@/components/Event/AddEvent";
|
||||
import LocalDate from "@/components/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 {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from "@nextui-org/modal";
|
||||
import { Select, SelectItem } from "@nextui-org/select";
|
||||
import {
|
||||
Select,
|
||||
SelectItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableColumn,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@nextui-org/table";
|
||||
import { Tooltip } from "@nextui-org/tooltip";
|
||||
Tooltip,
|
||||
} from "@nextui-org/react";
|
||||
import { useAsyncList } from "@react-stately/data";
|
||||
import React, { Key, useState } from "react";
|
||||
|
||||
|
||||
64
client/src/app/events/page.tsx
Normal file
64
client/src/app/events/page.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
import AddEvent from "@/components/Event/AddEvent";
|
||||
import AssignmentTable from "@/components/Event/AssignmentTable";
|
||||
import Event from "@/components/Event/Event";
|
||||
import { apiCall } from "@/lib";
|
||||
import zustand, { EventData } from "@/Zustand";
|
||||
import { Add } from "@carbon/icons-react";
|
||||
import { Button } from "@nextui-org/react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Events() {
|
||||
const [showAddItemDialogue, setShowAddItemDialogue] = useState(false);
|
||||
const events = zustand((state) => state.events);
|
||||
const admin = zustand((state) => state.user?.admin);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
console.debug("query");
|
||||
|
||||
const data = await apiCall<EventData[]>("GET", "events/assignments");
|
||||
|
||||
if (data.ok) {
|
||||
zustand.getState().setEvents(await data.json());
|
||||
}
|
||||
|
||||
return {
|
||||
items: [],
|
||||
};
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative flex-1">
|
||||
<h2 className="mb-4 text-center text-4xl">Upcoming Events</h2>
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
{events.map((ee, ii) => (
|
||||
<Event key={ii} event={ee}>
|
||||
<AssignmentTable tasks={ee.tasks} />
|
||||
</Event>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{admin ? (
|
||||
<>
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,12 +11,48 @@ export const metadata: Metadata = {
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export interface SiteLink {
|
||||
text: string;
|
||||
href: string;
|
||||
admin?: boolean;
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const footerSites = [
|
||||
const headerSites: SiteLink[] = [
|
||||
{
|
||||
text: "Overview",
|
||||
href: "/",
|
||||
},
|
||||
{
|
||||
text: "Events",
|
||||
href: "/events",
|
||||
},
|
||||
{
|
||||
text: "Assignments",
|
||||
href: "/assignments",
|
||||
},
|
||||
{
|
||||
text: "Assign Tasks",
|
||||
href: "/admin/assign",
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
text: "Users",
|
||||
href: "/admin/users",
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
text: "Configuration",
|
||||
href: "/admin/config",
|
||||
admin: true,
|
||||
},
|
||||
];
|
||||
|
||||
const footerSites: SiteLink[] = [
|
||||
{
|
||||
text: "Impressum",
|
||||
href: "/impressum",
|
||||
@@ -33,9 +69,9 @@ export default function RootLayout({
|
||||
<NextUIProvider>
|
||||
<div className="flex min-h-screen flex-col p-4">
|
||||
<header>
|
||||
<Header />
|
||||
<Header sites={headerSites} />
|
||||
</header>
|
||||
<main className="flex min-h-full flex-1 flex-col">
|
||||
<main className="flex min-h-full flex-1 flex-col p-4">
|
||||
<Main>{children}</Main>
|
||||
</main>
|
||||
<footer className="flex h-4 justify-center gap-4">
|
||||
|
||||
@@ -1,22 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { ViewFilled, ViewOffFilled } from "@carbon/icons-react";
|
||||
import { Button } from "@nextui-org/button";
|
||||
import { Form } from "@nextui-org/form";
|
||||
import { Input } from "@nextui-org/input";
|
||||
import { Switch } from "@nextui-org/switch";
|
||||
import { useState } from "react";
|
||||
import CheckboxIcon from "@/components/CheckboxIcon";
|
||||
import { apiCall } from "@/lib";
|
||||
import zustand from "@/Zustand";
|
||||
import {
|
||||
ViewFilled,
|
||||
ViewOffFilled,
|
||||
WarningHexFilled,
|
||||
} from "@carbon/icons-react";
|
||||
import { Alert, Button, Form, Input } from "@nextui-org/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { FormEvent, useState } from "react";
|
||||
|
||||
export default function Login() {
|
||||
const [visibility, setVisibility] = useState(false);
|
||||
const [wrongPassword, setWrongPassword] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
// set login-request
|
||||
async function sendLogin(e: FormEvent<HTMLFormElement>) {
|
||||
const data = Object.fromEntries(new FormData(e.currentTarget));
|
||||
|
||||
const result = await apiCall("POST", "login", undefined, data);
|
||||
|
||||
if (result.ok) {
|
||||
// add the user-info to the zustand
|
||||
zustand.getState().reset({ user: await result.json() });
|
||||
|
||||
// redirect to the home-page
|
||||
router.push("/");
|
||||
} else {
|
||||
setWrongPassword(true);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="mb-4 text-center text-4xl">Login</h2>
|
||||
<Form
|
||||
validationBehavior="native"
|
||||
className="flex flex-col items-center gap-2"
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
className="mx-auto flex max-w-sm flex-col items-center gap-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void sendLogin(e);
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
isRequired
|
||||
@@ -24,7 +51,6 @@ export default function Login() {
|
||||
label="Name"
|
||||
name="username"
|
||||
variant="bordered"
|
||||
className="max-w-xs"
|
||||
/>
|
||||
<Input
|
||||
isRequired
|
||||
@@ -32,7 +58,7 @@ export default function Login() {
|
||||
name="password"
|
||||
autoComplete="current-password"
|
||||
endContent={
|
||||
<Switch
|
||||
<CheckboxIcon
|
||||
className="my-auto"
|
||||
startContent={<ViewFilled />}
|
||||
endContent={<ViewOffFilled />}
|
||||
@@ -42,9 +68,18 @@ export default function Login() {
|
||||
}
|
||||
type={visibility ? "text" : "password"}
|
||||
variant="bordered"
|
||||
className="max-w-xs"
|
||||
/>
|
||||
<Button className="w-full max-w-xs" color="primary" type="submit">
|
||||
<Alert
|
||||
title="Login failed"
|
||||
description="Wrong username or password"
|
||||
color="danger"
|
||||
icon={<WarningHexFilled size={32} />}
|
||||
hideIconWrapper
|
||||
isClosable
|
||||
isVisible={wrongPassword}
|
||||
onVisibleChange={(v) => setWrongPassword(v)}
|
||||
/>
|
||||
<Button className="w-full" color="primary" type="submit">
|
||||
Login
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import Event from "@/components/Event/Event";
|
||||
import zustand, { Availabilities, Availability } from "@/Zustand";
|
||||
import { Radio, RadioGroup } from "@nextui-org/radio";
|
||||
|
||||
function availability2Color(availability: Availability) {
|
||||
switch (availability) {
|
||||
case "yes":
|
||||
return "success";
|
||||
case "maybe":
|
||||
return "warning";
|
||||
default:
|
||||
return "danger";
|
||||
}
|
||||
}
|
||||
|
||||
export default function OverviewPersonal() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="mb-4 text-center text-4xl">Upcoming Events</h2>
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
{zustand.getState().events.map((ev) => (
|
||||
<Event key={ev.id} event={ev}>
|
||||
<RadioGroup className="mt-auto" orientation="horizontal">
|
||||
{Availabilities.map((availability) => (
|
||||
<Radio
|
||||
value={availability}
|
||||
key={availability}
|
||||
color={availability2Color(availability)}
|
||||
classNames={{
|
||||
label: `text-${availability2Color(availability)}`,
|
||||
}}
|
||||
>
|
||||
{availability}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</Event>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import OverviewPersonal from "./me/page";
|
||||
import EventVolunteer from "./Overview";
|
||||
|
||||
export default function Home() {
|
||||
// return <EventVolunteer />;
|
||||
return <OverviewPersonal />;
|
||||
return <EventVolunteer />;
|
||||
}
|
||||
|
||||
33
client/src/components/CheckboxIcon.tsx
Normal file
33
client/src/components/CheckboxIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { SwitchProps, useSwitch, VisuallyHidden } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
export default function CheckboxIcon(props: SwitchProps) {
|
||||
const {
|
||||
Component,
|
||||
slots,
|
||||
isSelected,
|
||||
getBaseProps,
|
||||
getInputProps,
|
||||
getWrapperProps,
|
||||
} = useSwitch(props);
|
||||
|
||||
return (
|
||||
<Component {...getBaseProps()}>
|
||||
<VisuallyHidden>
|
||||
<input {...getInputProps()} />
|
||||
</VisuallyHidden>
|
||||
<div
|
||||
{...getWrapperProps()}
|
||||
className={slots.wrapper({
|
||||
class: [
|
||||
"h-8 w-8",
|
||||
"flex items-center justify-center",
|
||||
"rounded-lg bg-default-100 hover:bg-default-200",
|
||||
],
|
||||
})}
|
||||
>
|
||||
{isSelected ? props.startContent : props.endContent}
|
||||
</div>
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
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 {
|
||||
Button,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from "@nextui-org/modal";
|
||||
Textarea,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
interface state {
|
||||
date: ZonedDateTime;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardBody, CardHeader } from "@nextui-org/card";
|
||||
import { Divider } from "@nextui-org/divider";
|
||||
import LocalDate from "../LocalDate";
|
||||
import { EventData } from "@/Zustand";
|
||||
import { Card, CardBody, CardHeader, Divider } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
export default function Event({
|
||||
@@ -27,7 +26,6 @@ export default function Event({
|
||||
options={{
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
timeZone: "Europe/Berlin", // TODO: check with actual backend
|
||||
}}
|
||||
>
|
||||
{event.date}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use local";
|
||||
|
||||
import { DateFormatter } from "@/Zustand";
|
||||
import { parseZonedDateTime } from "@internationalized/date";
|
||||
import { getLocalTimeZone, parseDateTime } from "@internationalized/date";
|
||||
import { useLocale } from "@react-aria/i18n";
|
||||
|
||||
export default function LocalDate(props: {
|
||||
@@ -13,7 +13,9 @@ export default function LocalDate(props: {
|
||||
|
||||
return (
|
||||
<span className={props.className}>
|
||||
{formatter.format(parseZonedDateTime(props.children).toDate())}
|
||||
{formatter.format(
|
||||
parseDateTime(props.children).toDate(getLocalTimeZone()),
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
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";
|
||||
const HIGHLIGHT = "hsl(359,100%,65.7%)"; // #ff5053
|
||||
const FOREGROUND = "hsl(295,100%,97.5%)"; // #fef2ff
|
||||
const ACCENT1 = "hsl(246,100%,83.3%)"; // #b2aaff
|
||||
const ACCENT2 = "hsl(245,63.3%,61.6%)"; // #6a5fdb
|
||||
const ACCENT3 = "hsl(249,59.4%,25.1%)"; // #261a66
|
||||
const ACCENT4 = "hsl(264,63.4%,18.2%)"; // #29114c
|
||||
const ACCENT5 = "hsl(263,62.1%,11.4%)"; // #190b2f
|
||||
const BACKGROUND = "hsl(320,100%,2.9%)"; // #0f000a
|
||||
|
||||
export default {
|
||||
content: [
|
||||
@@ -21,20 +21,7 @@ export default {
|
||||
extend: {
|
||||
colors: {
|
||||
highlight: HIGHLIGHT,
|
||||
foreground: {
|
||||
DEFAULT: FOREGROUND,
|
||||
"50": FOREGROUND,
|
||||
"100": FOREGROUND,
|
||||
"200": FOREGROUND,
|
||||
"300": FOREGROUND,
|
||||
"400": FOREGROUND,
|
||||
"500": FOREGROUND,
|
||||
"600": "#fce8ff",
|
||||
"700": "#fad0fe",
|
||||
"800": "#f8abfc",
|
||||
"900": "#f579f9",
|
||||
"950": "#eb46ef",
|
||||
},
|
||||
foreground: FOREGROUND,
|
||||
"accent-1": ACCENT1,
|
||||
"accent-2": ACCENT2,
|
||||
"accent-3": ACCENT3,
|
||||
@@ -67,45 +54,45 @@ export default {
|
||||
// },
|
||||
primary: {
|
||||
DEFAULT: ACCENT2,
|
||||
"50": "#39357a",
|
||||
"100": "#42399a",
|
||||
"200": "#5144be",
|
||||
"300": ACCENT2,
|
||||
"400": "#6f6ee6",
|
||||
"500": "#8b91ee",
|
||||
"600": "#acb7f5",
|
||||
"700": "#cbd3fa",
|
||||
"800": "#e2e7fd",
|
||||
"900": "#eff3fe",
|
||||
"50": "hsl(244,100%,9.6%)",
|
||||
"100": "hsl(244,100%,19.2%)",
|
||||
"200": "hsl(244,100%,28.8%)",
|
||||
"300": "hsl(244,100%,38.4%)",
|
||||
"400": "hsl(244,100%,46.7%)",
|
||||
"500": "hsl(244,92.5%,58.4%)",
|
||||
"600": "hsl(244,92.5%,68.8%)",
|
||||
"700": "hsl(244,92.5%,79.2%)",
|
||||
"800": "hsl(244,92.5%,89.6%)",
|
||||
"900": "hsl(245,92.3%,94.9%)",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: ACCENT3,
|
||||
"50": "#3b288a",
|
||||
"100": "#462fa8",
|
||||
"200": "#5538c9",
|
||||
"300": "#634add",
|
||||
"400": "#776ae8",
|
||||
"500": "#9a95f0",
|
||||
"600": "#bdbcf6",
|
||||
"700": "#dadbfa",
|
||||
"800": "#ebebfc",
|
||||
"900": "#f4f4fe",
|
||||
"50": "hsl(249,66.7%,9.4%)",
|
||||
"100": "hsl(249,66.7%,18.8%)",
|
||||
"200": "hsl(249,66.7%,28.2%)",
|
||||
"300": "hsl(249,66.7%,37.6%)",
|
||||
"400": "hsl(249,66.7%,47.1%)",
|
||||
"500": "hsl(249,59.3%,57.6%)",
|
||||
"600": "hsl(249,59.3%,68.2%)",
|
||||
"700": "hsl(249,59.3%,78.8%)",
|
||||
"800": "hsl(249,59.3%,89.4%)",
|
||||
"900": "hsl(249,61.5%,94.9%)",
|
||||
},
|
||||
// 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",
|
||||
"50": "hsl(360,84.9%,10.4%)",
|
||||
"100": "hsl(359,86.5%,20.4%)",
|
||||
"200": "hsl(359,86%,30.8%)",
|
||||
"300": "hsl(359,86.5%,40.8%)",
|
||||
"400": "hsl(359,90.4%,51.2%)",
|
||||
"500": "hsl(359,90%,60.8%)",
|
||||
"600": "hsl(359,90.6%,70.8%)",
|
||||
"700": "hsl(359,90%,80.4%)",
|
||||
"800": "hsl(360,91.8%,90.4%)",
|
||||
"900": "hsl(359,92%,95.1%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user