Tailwind + daisy UI update #1
@@ -16,6 +16,11 @@ export default async ({ params }: { params: Promise<{ id: string }> }) => {
|
||||
publicId: true,
|
||||
},
|
||||
});
|
||||
const appointments = await prisma.eventAppointment.findMany({
|
||||
where: {
|
||||
eventId: parseInt(id),
|
||||
},
|
||||
});
|
||||
if (!event) return <div>Event not found</div>;
|
||||
return <Form event={event} users={users} />;
|
||||
return <Form event={event} users={users} appointments={appointments} />;
|
||||
};
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
'use client';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
EventAppointmentOptionalDefaults,
|
||||
EventAppointmentOptionalDefaultsSchema,
|
||||
EventOptionalDefaults,
|
||||
EventOptionalDefaultsSchema,
|
||||
ParticipantOptionalDefaultsSchema,
|
||||
} from '@repo/db/zod';
|
||||
import { set, useForm } from 'react-hook-form';
|
||||
import { BADGES, Event, User } from '@repo/db';
|
||||
import { Bot, FileText, UserIcon } from 'lucide-react';
|
||||
import { BADGES, Event, EventAppointment, User } from '@repo/db';
|
||||
import { Bot, Calendar, FileText, UserIcon } from 'lucide-react';
|
||||
import { Input } from '../../../../_components/ui/Input';
|
||||
import { useRef, useState } from 'react';
|
||||
import { deleteEvent, upsertEvent } from '../action';
|
||||
import { deleteEvent, upsertAppointment, upsertEvent } from '../action';
|
||||
import { Button } from '../../../../_components/ui/Button';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { redirect, useRouter } from 'next/navigation';
|
||||
import { Switch } from '../../../../_components/ui/Switch';
|
||||
import { PaginatedTable } from '../../../../_components/PaginatedTable';
|
||||
import {
|
||||
PaginatedTable,
|
||||
PaginatedTableRef,
|
||||
} from '../../../../_components/PaginatedTable';
|
||||
import { Select } from '../../../../_components/ui/Select';
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
export const Form = ({
|
||||
event,
|
||||
users,
|
||||
appointments = [],
|
||||
}: {
|
||||
event?: Event;
|
||||
users: {
|
||||
@@ -29,15 +35,21 @@ export const Form = ({
|
||||
lastname: string;
|
||||
publicId: string;
|
||||
}[];
|
||||
appointments?: EventAppointment[];
|
||||
}) => {
|
||||
const { data: session } = useSession();
|
||||
const form = useForm({
|
||||
resolver: zodResolver(EventOptionalDefaultsSchema),
|
||||
defaultValues: event,
|
||||
});
|
||||
const appointmentForm = useForm({
|
||||
const appointmentForm = useForm<EventAppointmentOptionalDefaults>({
|
||||
resolver: zodResolver(EventAppointmentOptionalDefaultsSchema),
|
||||
defaultValues: {
|
||||
eventId: event?.id,
|
||||
presenterId: session?.user?.id,
|
||||
},
|
||||
});
|
||||
console.log(appointmentForm.formState.errors);
|
||||
const appointmentsTableRef = useRef<PaginatedTableRef>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const addParticipantModal = useRef<HTMLDialogElement>(null);
|
||||
@@ -51,20 +63,26 @@ export const Form = ({
|
||||
✕
|
||||
</button>
|
||||
</form>
|
||||
<h3 className="font-bold text-lg">Teilnehmer hinzufügen</h3>
|
||||
<h3 className="font-bold text-lg">Termin hinzufügen</h3>
|
||||
<form
|
||||
onSubmit={appointmentForm.handleSubmit(async (values) => {
|
||||
console.log(values);
|
||||
if (!event) return;
|
||||
const createdAppointment = await upsertAppointment({
|
||||
appointmentDate: values.appointmentDate,
|
||||
eventId: event?.id,
|
||||
presenterId: values.presenterId,
|
||||
});
|
||||
console.log(createdAppointment);
|
||||
addParticipantModal.current?.close();
|
||||
appointmentsTableRef.current?.refresh();
|
||||
})}
|
||||
className="flex flex-col"
|
||||
>
|
||||
<Select
|
||||
<Input
|
||||
form={appointmentForm}
|
||||
name="userId"
|
||||
label="Teilnehmer"
|
||||
options={users.map((user) => ({
|
||||
label: `${user.firstname} ${user.lastname} (${user.publicId})`,
|
||||
value: user.id,
|
||||
}))}
|
||||
label="Datum"
|
||||
name="appointmentDate"
|
||||
type="date"
|
||||
/>
|
||||
<div className="modal-action">
|
||||
<Button type="submit" className="btn btn-primary">
|
||||
@@ -154,7 +172,7 @@ export const Form = ({
|
||||
<div className="card-body">
|
||||
<div className="flex justify-between">
|
||||
<h2 className="card-title">
|
||||
<UserIcon className="w-5 h-5" /> Termine
|
||||
<Calendar className="w-5 h-5" /> Termine
|
||||
</h2>
|
||||
<button
|
||||
className="btn btn-primary btn-outline"
|
||||
@@ -165,16 +183,66 @@ export const Form = ({
|
||||
</div>
|
||||
|
||||
<PaginatedTable
|
||||
ref={appointmentsTableRef}
|
||||
prismaModel={'eventAppointment'}
|
||||
filter={{
|
||||
eventId: event?.id,
|
||||
}}
|
||||
include={[
|
||||
include={{
|
||||
Presenter: true,
|
||||
Participants: true,
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
user: true,
|
||||
header: 'Datum',
|
||||
accessorKey: 'appointmentDate',
|
||||
accessorFn: (date) =>
|
||||
new Date(date.appointmentDate).toLocaleDateString(),
|
||||
},
|
||||
{
|
||||
header: 'Presenter',
|
||||
accessorKey: 'presenter',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center">
|
||||
<span className="ml-2">
|
||||
{(row.original as any).Presenter.firstname}{' '}
|
||||
{(row.original as any).Presenter.lastname}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Teilnehmer',
|
||||
accessorKey: 'Participants',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center">
|
||||
<UserIcon className="w-5 h-5" />
|
||||
<span className="ml-2">
|
||||
{row.original.Participants.length}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Aktionen',
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onSubmit={() => false}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
console.log(row.original);
|
||||
}}
|
||||
className="btn btn-sm btn-outline"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
columns={[]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use server';
|
||||
|
||||
import { prisma, Prisma, Event, Participant } from '@repo/db';
|
||||
import { prisma, Prisma, Event, Participant, EventAppointment } from '@repo/db';
|
||||
|
||||
export const upsertEvent = async (
|
||||
event: Prisma.EventCreateInput,
|
||||
@@ -19,6 +19,34 @@ export const deleteEvent = async (id: Event['id']) => {
|
||||
await prisma.event.delete({ where: { id: id } });
|
||||
};
|
||||
|
||||
export const upsertAppointment = async (
|
||||
eventAppointment: Prisma.XOR<
|
||||
Prisma.EventAppointmentCreateInput,
|
||||
Prisma.EventAppointmentUncheckedCreateInput
|
||||
>,
|
||||
id?: EventAppointment['id']
|
||||
) => {
|
||||
const newEventAppointment = id
|
||||
? await prisma.eventAppointment.update({
|
||||
where: { id: id },
|
||||
data: eventAppointment,
|
||||
})
|
||||
: await prisma.eventAppointment.create({ data: eventAppointment });
|
||||
return newEventAppointment;
|
||||
};
|
||||
|
||||
export const deleteAppoinement = async (id: Event['id']) => {
|
||||
await prisma.eventAppointment.delete({ where: { id: id } });
|
||||
prisma.eventAppointment.findMany({
|
||||
where: {
|
||||
eventId: id,
|
||||
},
|
||||
orderBy: {
|
||||
// TODO: add order by in relation to table selected column
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const upsertParticipant = async (
|
||||
participant: Prisma.ParticipantCreateInput,
|
||||
id?: Participant['id']
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { prisma } from '@repo/db';
|
||||
import { Form } from '../_components/Form';
|
||||
|
||||
export default () => {
|
||||
return <Form />;
|
||||
export default async () => {
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
firstname: true,
|
||||
lastname: true,
|
||||
publicId: true,
|
||||
},
|
||||
});
|
||||
return <Form users={users} />;
|
||||
};
|
||||
|
||||
@@ -5,16 +5,6 @@ import Link from 'next/link';
|
||||
export default () => {
|
||||
return (
|
||||
<>
|
||||
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<PartyPopperIcon className="w-5 h-5" /> Events
|
||||
</span>
|
||||
<Link href={'/admin/event/new'}>
|
||||
<button className="btn btn-sm btn-outline btn-primary">
|
||||
Erstellen
|
||||
</button>
|
||||
</Link>
|
||||
</p>
|
||||
<PaginatedTable
|
||||
showEditButton
|
||||
prismaModel="event"
|
||||
@@ -28,6 +18,18 @@ export default () => {
|
||||
accessorKey: 'hidden',
|
||||
},
|
||||
]}
|
||||
leftOfSearch={
|
||||
<span className="flex items-center gap-2">
|
||||
<PartyPopperIcon className="w-5 h-5" /> Events
|
||||
</span>
|
||||
}
|
||||
rightOfSearch={
|
||||
<Link href={'/admin/event/new'}>
|
||||
<button className="btn btn-sm btn-outline btn-primary">
|
||||
Erstellen
|
||||
</button>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,42 +1,46 @@
|
||||
import { DatabaseBackupIcon } from "lucide-react";
|
||||
import { PaginatedTable } from "../../../_components/PaginatedTable";
|
||||
import Link from "next/link";
|
||||
import { DatabaseBackupIcon } from 'lucide-react';
|
||||
import { PaginatedTable } from '../../../_components/PaginatedTable';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<>
|
||||
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<DatabaseBackupIcon className="w-5 h-5" /> Stationen
|
||||
</span>
|
||||
<Link href={"/admin/station/new"}>
|
||||
<button className="btn btn-sm btn-outline btn-primary">
|
||||
Erstellen
|
||||
</button>
|
||||
</Link>
|
||||
</p>
|
||||
<PaginatedTable
|
||||
showEditButton
|
||||
prismaModel="station"
|
||||
searchFields={["bosCallsign", "bosUse", "country", "operator"]}
|
||||
searchFields={['bosCallsign', 'bosUse', 'country', 'operator']}
|
||||
columns={[
|
||||
{
|
||||
header: "BOS Name",
|
||||
accessorKey: "bosCallsign",
|
||||
header: 'BOS Name',
|
||||
accessorKey: 'bosCallsign',
|
||||
},
|
||||
{
|
||||
header: "Bos Use",
|
||||
accessorKey: "bosUse",
|
||||
header: 'Bos Use',
|
||||
accessorKey: 'bosUse',
|
||||
},
|
||||
{
|
||||
header: "Country",
|
||||
accessorKey: "country",
|
||||
header: 'Country',
|
||||
accessorKey: 'country',
|
||||
},
|
||||
{
|
||||
header: "operator",
|
||||
accessorKey: "operator",
|
||||
header: 'operator',
|
||||
accessorKey: 'operator',
|
||||
},
|
||||
]}
|
||||
leftOfSearch={
|
||||
<span className="flex items-center gap-2">
|
||||
<DatabaseBackupIcon className="w-5 h-5" /> Stationen
|
||||
</span>
|
||||
}
|
||||
rightOfSearch={
|
||||
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between">
|
||||
<Link href={'/admin/station/new'}>
|
||||
<button className="btn btn-sm btn-outline btn-primary">
|
||||
Erstellen
|
||||
</button>
|
||||
</Link>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
import { User2 } from "lucide-react";
|
||||
import { PaginatedTable } from "../../../_components/PaginatedTable";
|
||||
import { User2 } from 'lucide-react';
|
||||
import { PaginatedTable } from '../../../_components/PaginatedTable';
|
||||
|
||||
export default async () => {
|
||||
return (
|
||||
<>
|
||||
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
||||
<User2 className="w-5 h-5" /> Benutzer
|
||||
</p>
|
||||
<PaginatedTable
|
||||
showEditButton
|
||||
prismaModel="user"
|
||||
searchFields={["publicId", "firstname", "lastname", "email"]}
|
||||
searchFields={['publicId', 'firstname', 'lastname', 'email']}
|
||||
columns={[
|
||||
{
|
||||
header: "ID",
|
||||
accessorKey: "publicId",
|
||||
header: 'ID',
|
||||
accessorKey: 'publicId',
|
||||
},
|
||||
{
|
||||
header: "Vorname",
|
||||
accessorKey: "firstname",
|
||||
header: 'Vorname',
|
||||
accessorKey: 'firstname',
|
||||
},
|
||||
{
|
||||
header: "Nachname",
|
||||
accessorKey: "lastname",
|
||||
header: 'Nachname',
|
||||
accessorKey: 'lastname',
|
||||
},
|
||||
{
|
||||
header: "Email",
|
||||
accessorKey: "email",
|
||||
header: 'Email',
|
||||
accessorKey: 'email',
|
||||
},
|
||||
]}
|
||||
leftOfSearch={
|
||||
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
||||
<User2 className="w-5 h-5" /> Benutzer
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,8 +4,11 @@ import {
|
||||
InstagramLogoIcon,
|
||||
ReaderIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { HorizontalNav, VerticalNav } from "../_components/ui/Nav";
|
||||
import { HorizontalNav, VerticalNav } from "../_components/Nav";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getServerSession } from "../api/auth/[...nextauth]/auth";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
@@ -17,6 +20,10 @@ export default async function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const session = await getServerSession();
|
||||
|
||||
if (!session) redirect(`/login`);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hero min-h-screen"
|
||||
@@ -35,7 +42,7 @@ export default async function RootLayout({
|
||||
<HorizontalNav />
|
||||
|
||||
{/* Hauptlayout: Sidebar + Content (nimmt Resthöhe ein) */}
|
||||
<div className="flex flex-grow overflow-hidden">
|
||||
<div className="flex grow overflow-hidden">
|
||||
{/* Linke Sidebar */}
|
||||
<VerticalNav />
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ const AuthLayout: NextPage<
|
||||
'url(https://img.daisyui.com/images/stock/photo-1507358522600-9f71e620c44e.webp)',
|
||||
}}
|
||||
>
|
||||
<div className="hero-overlay bg-opacity-60"></div>
|
||||
<div className="hero-content text-neutral-content text-center ">
|
||||
<div className="hero-overlay bg-neutral/60"></div>
|
||||
<div className="hero-content text-center ">
|
||||
<div className="max-w-lg">
|
||||
<div className="card bg-base-100 w-full min-w-[500px] shadow-2xl max-md:min-w-[400px]">
|
||||
<div className="card rounded-2xl bg-base-100 w-full min-w-[500px] shadow-2xl max-md:min-w-[400px]">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import Link from 'next/link';
|
||||
import { redirect, useSearchParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Toaster, toast } from 'react-hot-toast';
|
||||
import { z } from 'zod';
|
||||
"use client";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { signIn } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { redirect, useSearchParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Toaster, toast } from "react-hot-toast";
|
||||
import { z } from "zod";
|
||||
|
||||
export const Login = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -27,24 +27,22 @@ export const Login = () => {
|
||||
className="card-body"
|
||||
onSubmit={form.handleSubmit(async () => {
|
||||
setIsLoading(true);
|
||||
const data = await signIn('credentials', {
|
||||
const data = await signIn("credentials", {
|
||||
redirect: false,
|
||||
email: form.getValues('email'),
|
||||
password: form.getValues('password'),
|
||||
email: form.getValues("email"),
|
||||
password: form.getValues("password"),
|
||||
});
|
||||
setIsLoading(false);
|
||||
if (!data || data.error) {
|
||||
toast.error('E-Mail / Passwort ist falsch!', {
|
||||
toast.error("E-Mail / Passwort ist falsch!", {
|
||||
style: {
|
||||
background:
|
||||
'var(--fallback-b1, oklch(var(--b1) / var(--tw-bg-opacity, 1)))',
|
||||
color:
|
||||
'var(--fallback-nc, oklch(var(--nc) / var(--tw-text-opacity, 1)))',
|
||||
background: "var(--color-base-100)",
|
||||
color: "var(--color-base-content)",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
redirect(searchParams.get('redirect') || '/');
|
||||
redirect(searchParams.get("redirect") || "/");
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
@@ -52,12 +50,12 @@ export const Login = () => {
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold">Login</h1>
|
||||
<span className="text-sm font-medium">
|
||||
Noch keinen Account? Zur{' '}
|
||||
Noch keinen Account? Zur{" "}
|
||||
<Link href="/register" className="link link-accent link-hover">
|
||||
Registrierung
|
||||
</Link>
|
||||
</span>
|
||||
<label className="input input-bordered flex items-center gap-2">
|
||||
<label className="input input-bordered flex items-center gap-2 w-full">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
@@ -70,16 +68,16 @@ export const Login = () => {
|
||||
<input
|
||||
type="text"
|
||||
className="grow"
|
||||
{...form.register('email')}
|
||||
{...form.register("email")}
|
||||
placeholder="Email"
|
||||
/>
|
||||
</label>
|
||||
<p className="text-error">
|
||||
{typeof form.formState.errors.email?.message === 'string'
|
||||
{typeof form.formState.errors.email?.message === "string"
|
||||
? form.formState.errors.email.message
|
||||
: ''}
|
||||
: ""}
|
||||
</p>
|
||||
<label className="input input-bordered flex items-center gap-2 mt-2">
|
||||
<label className="input input-bordered flex items-center gap-2 mt-2 w-full">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
@@ -95,21 +93,17 @@ export const Login = () => {
|
||||
<input
|
||||
autoComplete="current-password"
|
||||
type="password"
|
||||
{...form.register('password')}
|
||||
{...form.register("password")}
|
||||
placeholder="Passwort"
|
||||
className="grow"
|
||||
/>
|
||||
</label>
|
||||
<div className="form-control mt-6">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
name="loginBtn"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<div className="card-actions mt-6">
|
||||
<button className="btn btn-primary btn-block" disabled={isLoading}>
|
||||
{isLoading && (
|
||||
<span className="loading loading-spinner loading-sm"></span>
|
||||
)}
|
||||
Login{isLoading && '...'}
|
||||
Login{isLoading && "..."}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -71,7 +71,7 @@ export const Register = () => {
|
||||
</Link>
|
||||
</span>
|
||||
<div className="mt-5 mb-2">
|
||||
<label className="input input-bordered flex items-center gap-2 mt-2">
|
||||
<label className="input input-bordered flex items-center gap-2 mt-2 w-full">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
@@ -92,7 +92,7 @@ export const Register = () => {
|
||||
? form.formState.errors.firstname.message
|
||||
: ''}
|
||||
</p>
|
||||
<label className="input input-bordered flex items-center gap-2 mt-2">
|
||||
<label className="input input-bordered flex items-center gap-2 mt-2 w-full">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
@@ -113,8 +113,8 @@ export const Register = () => {
|
||||
? form.formState.errors.lastname.message
|
||||
: ''}
|
||||
</p>
|
||||
<div className="divider divider-neutral">Account</div>
|
||||
<label className="input input-bordered flex items-center gap-2">
|
||||
<div className="divider">Account</div>
|
||||
<label className="input input-bordered flex items-center gap-2 w-full">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
@@ -137,7 +137,7 @@ export const Register = () => {
|
||||
? form.formState.errors.email.message
|
||||
: ''}
|
||||
</p>
|
||||
<label className="input input-bordered flex items-center gap-2 mt-2">
|
||||
<label className="input input-bordered flex items-center gap-2 mt-2 w-full">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
@@ -163,7 +163,7 @@ export const Register = () => {
|
||||
? form.formState.errors.password.message
|
||||
: ''}
|
||||
</p>
|
||||
<label className="input input-bordered flex items-center gap-2 mt-2">
|
||||
<label className="input input-bordered flex items-center gap-2 mt-2 w-full">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
@@ -189,9 +189,9 @@ export const Register = () => {
|
||||
? form.formState.errors.passwordConfirm.message
|
||||
: ''}
|
||||
</p>
|
||||
<div className="form-control mt-6">
|
||||
<div className="card-actions mt-6">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="btn btn-primary btn-block"
|
||||
name="registerBtn"
|
||||
disabled={isLoading}
|
||||
>
|
||||
|
||||
78
apps/hub/app/_components/Nav.tsx
Normal file
78
apps/hub/app/_components/Nav.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
HomeIcon,
|
||||
PersonIcon,
|
||||
GearIcon,
|
||||
ExitIcon,
|
||||
LockClosedIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
export const VerticalNav = () => {
|
||||
return (
|
||||
<ul className="menu w-64 bg-base-300 p-4 rounded-lg shadow-md">
|
||||
<li>
|
||||
<Link href="/">
|
||||
<HomeIcon /> Dashboard
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/profile">
|
||||
<PersonIcon /> Profile
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<details open>
|
||||
<summary>
|
||||
<LockClosedIcon />
|
||||
Admin
|
||||
</summary>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/admin/user">Benutzer</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/admin/station">Stationen</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/admin/event">Events</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/settings">
|
||||
<GearIcon />
|
||||
Einstellungen
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export const HorizontalNav = () => (
|
||||
<div className="navbar bg-base-200 shadow-md rounded-lg mb-4">
|
||||
<div className="flex items-center">
|
||||
<a className="btn btn-ghost normal-case text-xl">
|
||||
Virtual Air Rescue - HUB
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center ml-auto">
|
||||
<ul className="flex space-x-2 px-1">
|
||||
<li>
|
||||
<Link href="/">
|
||||
<button className="btn btn-sm btn-outline btn-primary">
|
||||
Zur Leitstelle
|
||||
</button>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/logout">
|
||||
<button className="btn btn-sm btn-ghost">
|
||||
<ExitIcon /> Logout
|
||||
</button>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1,9 +1,19 @@
|
||||
'use client';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
Ref,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import SortableTable, { Pagination, SortableTableProps } from './Table';
|
||||
import { PrismaClient } from '@repo/db';
|
||||
import { getData } from './pagiantedTableActions';
|
||||
|
||||
export interface PaginatedTableRef {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
interface PaginatedTableProps<TData>
|
||||
extends Omit<SortableTableProps<TData>, 'data'> {
|
||||
prismaModel: keyof PrismaClient;
|
||||
@@ -11,7 +21,10 @@ interface PaginatedTableProps<TData>
|
||||
rowsPerPage?: number;
|
||||
showEditButton?: boolean;
|
||||
searchFields?: string[];
|
||||
include?: Record<string, boolean>[];
|
||||
include?: Record<string, boolean>;
|
||||
leftOfSearch?: React.ReactNode;
|
||||
rightOfSearch?: React.ReactNode;
|
||||
ref?: Ref<PaginatedTableRef>;
|
||||
}
|
||||
|
||||
export function PaginatedTable<TData>({
|
||||
@@ -21,6 +34,9 @@ export function PaginatedTable<TData>({
|
||||
searchFields = [],
|
||||
filter,
|
||||
include,
|
||||
ref,
|
||||
leftOfSearch,
|
||||
rightOfSearch,
|
||||
...restProps
|
||||
}: PaginatedTableProps<TData>) {
|
||||
const [data, setData] = useState<TData[]>([]);
|
||||
@@ -29,6 +45,30 @@ export function PaginatedTable<TData>({
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm);
|
||||
|
||||
const RefreshTableData = async () => {
|
||||
getData(
|
||||
prismaModel,
|
||||
rowsPerPage,
|
||||
page * rowsPerPage,
|
||||
debouncedSearchTerm,
|
||||
searchFields,
|
||||
filter,
|
||||
include
|
||||
).then((result) => {
|
||||
if (result) {
|
||||
setData(result.data);
|
||||
setTotal(result.total);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
refresh: () => {
|
||||
console.log('refresh');
|
||||
RefreshTableData();
|
||||
},
|
||||
}));
|
||||
|
||||
const debounce = (func: Function, delay: number) => {
|
||||
let timer: NodeJS.Timeout;
|
||||
return (...args: any[]) => {
|
||||
@@ -45,25 +85,14 @@ export function PaginatedTable<TData>({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
getData(
|
||||
prismaModel,
|
||||
rowsPerPage,
|
||||
page * rowsPerPage,
|
||||
debouncedSearchTerm,
|
||||
searchFields,
|
||||
filter
|
||||
).then((result) => {
|
||||
if (result) {
|
||||
setData(result.data);
|
||||
setTotal(result.total);
|
||||
}
|
||||
});
|
||||
RefreshTableData();
|
||||
}, [page, debouncedSearchTerm]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 m-4">
|
||||
{searchFields.length > 0 && (
|
||||
<div className="flex justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1">{leftOfSearch}</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Suchen..."
|
||||
@@ -74,6 +103,7 @@ export function PaginatedTable<TData>({
|
||||
}}
|
||||
className="input input-bordered w-full max-w-xs justify-end"
|
||||
/>
|
||||
<div className="flex justify-center">{rightOfSearch}</div>
|
||||
</div>
|
||||
)}
|
||||
<SortableTable
|
||||
|
||||
@@ -116,7 +116,7 @@ export const Pagination = ({
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
<select
|
||||
className="select join-item"
|
||||
className="select join-item w-16"
|
||||
value={page}
|
||||
onChange={(e) => setPage(Number(e.target.value))}
|
||||
>
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function getData(
|
||||
searchTerm: string,
|
||||
searchFields: string[],
|
||||
filter?: Record<string, any>,
|
||||
include?: Record<string, boolean>[]
|
||||
include?: Record<string, boolean>
|
||||
) {
|
||||
if (!model || !prisma[model]) {
|
||||
return { data: [], total: 0 };
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import 'tailwindcss';
|
||||
@plugin "daisyui";
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
/* --p: 47.67% 0.2484 267.02; */
|
||||
--nc: #a6adbb;
|
||||
}
|
||||
|
||||
body.modal-open {
|
||||
|
||||
@@ -33,16 +33,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4.0.8",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/jsonwebtoken": "^9.0.8",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"daisyui": "^4.12.23",
|
||||
"daisyui": "^5.0.0-beta.8",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.4",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss": "^4.0.8",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
export default {
|
||||
content: [
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
plugins: [require('daisyui')],
|
||||
} satisfies Config;
|
||||
1005
package-lock.json
generated
1005
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `_EventAppointmentToUser` table. If the table is not empty, all the data it contains will be lost.
|
||||
- Added the required column `presenterId` to the `EventAppointment` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "_EventAppointmentToUser" DROP CONSTRAINT "_EventAppointmentToUser_A_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "_EventAppointmentToUser" DROP CONSTRAINT "_EventAppointmentToUser_B_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "EventAppointment" ADD COLUMN "presenterId" TEXT NOT NULL;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "_EventAppointmentToUser";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_EventAppointmentUser" (
|
||||
"A" INTEGER NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "_EventAppointmentUser_AB_pkey" PRIMARY KEY ("A","B")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_EventAppointmentUser_B_index" ON "_EventAppointmentUser"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "EventAppointment" ADD CONSTRAINT "EventAppointment_presenterId_fkey" FOREIGN KEY ("presenterId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_EventAppointmentUser" ADD CONSTRAINT "_EventAppointmentUser_A_fkey" FOREIGN KEY ("A") REFERENCES "EventAppointment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_EventAppointmentUser" ADD CONSTRAINT "_EventAppointmentUser_B_fkey" FOREIGN KEY ("B") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -17,7 +17,7 @@ model EventAppointment {
|
||||
presenterId String
|
||||
// relations:
|
||||
Users User[] @relation("EventAppointmentUser")
|
||||
participants Participant[]
|
||||
Participants Participant[]
|
||||
Event Event @relation(fields: [eventId], references: [id])
|
||||
Presenter User @relation(fields: [presenterId], references: [id])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user