This commit is contained in:
Nicolas
2025-02-28 03:13:50 +01:00
107 changed files with 18565 additions and 17809 deletions

View File

@@ -1,26 +1,13 @@
import { prisma } from '@repo/db';
import { Form } from '../_components/Form';
import { prisma } from "@repo/db";
import { Form } from "../_components/Form";
export default async ({ params }: { params: Promise<{ id: string }> }) => {
const { id } = await params;
const event = await prisma.event.findUnique({
where: {
id: parseInt(id),
},
});
const users = await prisma.user.findMany({
select: {
id: true,
firstname: true,
lastname: true,
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} appointments={appointments} />;
const { id } = await params;
const event = await prisma.event.findUnique({
where: {
id: parseInt(id),
},
});
if (!event) return <div>Event not found</div>;
return <Form event={event} />;
};

View File

@@ -1,277 +1,264 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
"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, 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, upsertAppointment, upsertEvent } from '../action';
import { Button } from '../../../../_components/ui/Button';
import { redirect, useRouter } from 'next/navigation';
import { Switch } from '../../../../_components/ui/Switch';
EventAppointmentOptionalDefaults,
EventAppointmentOptionalDefaultsSchema,
EventOptionalDefaults,
EventOptionalDefaultsSchema,
ParticipantOptionalDefaultsSchema,
} from "@repo/db/zod";
import { set, useForm } from "react-hook-form";
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, upsertAppointment, upsertEvent } from "../action";
import { Button } from "../../../../_components/ui/Button";
import { redirect, useRouter } from "next/navigation";
import { Switch } from "../../../../_components/ui/Switch";
import {
PaginatedTable,
PaginatedTableRef,
} from '../../../../_components/PaginatedTable';
import { Select } from '../../../../_components/ui/Select';
import { useSession } from 'next-auth/react';
import { MarkdownEditor } from '../../../../_components/ui/MDEditor';
PaginatedTable,
PaginatedTableRef,
} from "../../../../_components/PaginatedTable";
import { Select } from "../../../../_components/ui/Select";
import { useSession } from "next-auth/react";
import { MarkdownEditor } from "../../../../_components/ui/MDEditor";
export const Form = ({
event,
users,
appointments = [],
}: {
event?: Event;
users: {
id: string;
firstname: string;
lastname: string;
publicId: string;
}[];
appointments?: EventAppointment[];
}) => {
const { data: session } = useSession();
const form = useForm({
resolver: zodResolver(EventOptionalDefaultsSchema),
defaultValues: event,
});
const appointmentForm = useForm<EventAppointmentOptionalDefaults>({
resolver: zodResolver(EventAppointmentOptionalDefaultsSchema),
defaultValues: {
eventId: event?.id,
presenterId: session?.user?.id,
},
});
const appointmentsTableRef = useRef<PaginatedTableRef>(null);
const [loading, setLoading] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
const addParticipantModal = useRef<HTMLDialogElement>(null);
return (
<>
<dialog ref={addParticipantModal} className="modal">
<div className="modal-box">
<form method="dialog">
{/* if there is a button in form, it will close the modal */}
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
</button>
</form>
<h3 className="font-bold text-lg">Termin hinzufügen</h3>
<form
onSubmit={appointmentForm.handleSubmit(async (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"
>
<Input
form={appointmentForm}
label="Datum"
name="appointmentDate"
type="date"
/>
<div className="modal-action">
<Button type="submit" className="btn btn-primary">
Hinzufügen
</Button>
</div>
</form>
</div>
</dialog>
<form
onSubmit={form.handleSubmit(async (values) => {
setLoading(true);
const createdEvent = await upsertEvent(values, event?.id);
setLoading(false);
if (!event) redirect(`/admin/event`);
})}
className="grid grid-cols-6 gap-3"
>
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
<div className="card-body">
<h2 className="card-title">
<FileText className="w-5 h-5" /> Allgemeines
</h2>
<Input form={form} label="Name" name="name" className="input-sm" />
<MarkdownEditor form={form} name="description" />
<Input
form={form}
label="Maximale Teilnehmer (Nur für live Events)"
className="input-sm"
{...form.register('maxParticipants', {
valueAsNumber: true,
})}
/>
<Switch form={form} name="hidden" label="Versteckt" />
</div>
</div>
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
<div className="card-body">
<h2 className="card-title">
<Bot className="w-5 h-5" /> Automation
</h2>
<Input
form={form}
name="starterMoodleCourseId"
label="Moodle Anmelde Kurs ID"
className="input-sm"
/>
<Input
name="finisherMoodleCourseId"
form={form}
label="Moodle Abschluss Kurs ID"
className="input-sm"
/>
<Select
isMulti
form={form}
name="finishedBadges"
label="Badges bei Abschluss"
options={Object.entries(BADGES).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
<Select
isMulti
form={form}
name="requiredBadges"
label="Benötigte Badges"
options={Object.entries(BADGES).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
<Switch
form={form}
name="hasPresenceEvents"
label="Hat Live Event"
/>
</div>
</div>
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
<div className="card-body">
<div className="flex justify-between">
<h2 className="card-title">
<Calendar className="w-5 h-5" /> Termine
</h2>
{event && (
<button
className="btn btn-primary btn-outline"
onClick={() => addParticipantModal.current?.showModal()}
>
Hinzufügen
</button>
)}
</div>
export const Form = ({ event }: { event?: Event }) => {
const { data: session } = useSession();
const form = useForm({
resolver: zodResolver(EventOptionalDefaultsSchema),
defaultValues: event,
});
const appointmentForm = useForm<EventAppointmentOptionalDefaults>({
resolver: zodResolver(EventAppointmentOptionalDefaultsSchema),
defaultValues: {
eventId: event?.id,
presenterId: session?.user?.id,
},
});
const appointmentsTableRef = useRef<PaginatedTableRef>(null);
const [loading, setLoading] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
const addParticipantModal = useRef<HTMLDialogElement>(null);
return (
<>
<dialog ref={addParticipantModal} className="modal">
<div className="modal-box">
<form method="dialog">
{/* if there is a button in form, it will close the modal */}
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
</button>
</form>
<h3 className="font-bold text-lg">Termin hinzufügen</h3>
<form
onSubmit={appointmentForm.handleSubmit(async (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"
>
<Input
form={appointmentForm}
label="Datum"
name="appointmentDate"
type="date"
/>
<div className="modal-action">
<Button type="submit" className="btn btn-primary">
Hinzufügen
</Button>
</div>
</form>
</div>
</dialog>
<form
onSubmit={form.handleSubmit(async (values) => {
setLoading(true);
const createdEvent = await upsertEvent(values, event?.id);
setLoading(false);
if (!event) redirect(`/admin/event`);
})}
className="grid grid-cols-6 gap-3"
>
<div className="card bg-base-200 shadow-xl col-span-3 max-xl:col-span-6">
<div className="card-body">
<h2 className="card-title">
<FileText className="w-5 h-5" /> Allgemeines
</h2>
<Input form={form} label="Name" name="name" className="input-sm" />
<MarkdownEditor form={form} name="description" />
<Input
form={form}
label="Maximale Teilnehmer (Nur für live Events)"
className="input-sm"
{...form.register("maxParticipants", {
valueAsNumber: true,
})}
/>
<Switch form={form} name="hidden" label="Versteckt" />
</div>
</div>
<div className="card bg-base-200 shadow-xl col-span-3 max-xl:col-span-6">
<div className="card-body">
<h2 className="card-title">
<Bot className="w-5 h-5" /> Automation
</h2>
<Input
form={form}
name="starterMoodleCourseId"
label="Moodle Anmelde Kurs ID"
className="input-sm"
/>
<Input
name="finisherMoodleCourseId"
form={form}
label="Moodle Abschluss Kurs ID"
className="input-sm"
/>
<Select
isMulti
form={form}
name="finishedBadges"
label="Badges bei Abschluss"
options={Object.entries(BADGES).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
<Select
isMulti
form={form}
name="requiredBadges"
label="Benötigte Badges"
options={Object.entries(BADGES).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
<Switch
form={form}
name="hasPresenceEvents"
label="Hat Live Event"
/>
</div>
</div>
<div className="card bg-base-200 shadow-xl col-span-6">
<div className="card-body">
<div className="flex justify-between">
<h2 className="card-title">
<Calendar className="w-5 h-5" /> Termine
</h2>
{event && (
<button
className="btn btn-primary btn-outline"
onClick={() => addParticipantModal.current?.showModal()}
>
Hinzufügen
</button>
)}
</div>
<PaginatedTable
ref={appointmentsTableRef}
prismaModel={'eventAppointment'}
filter={{
eventId: event?.id,
}}
include={{
Presenter: true,
Participants: true,
}}
columns={[
{
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);
// TODO: open modal to edit appointment
}}
className="btn btn-sm btn-outline"
>
Bearbeiten
</button>
</div>
);
},
},
]}
/>
</div>
</div>
<div className="card bg-base-200 shadow-xl col-span-6">
<div className="card-body ">
<div className="flex w-full gap-4">
<Button
isLoading={loading}
type="submit"
className="btn btn-primary flex-1"
>
Speichern
</Button>
{event && (
<Button
isLoading={deleteLoading}
onClick={async () => {
setDeleteLoading(true);
await deleteEvent(event.id);
redirect('/admin/event');
}}
className="btn btn-error"
>
Löschen
</Button>
)}
</div>
</div>
</div>
</form>
</>
);
<PaginatedTable
ref={appointmentsTableRef}
prismaModel={"eventAppointment"}
filter={{
eventId: event?.id,
}}
include={{
Presenter: true,
Participants: true,
}}
columns={[
{
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);
// TODO: open modal to edit appointment
}}
className="btn btn-sm btn-outline"
>
Bearbeiten
</button>
</div>
);
},
},
]}
/>
</div>
</div>
<div className="card bg-base-200 shadow-xl col-span-6">
<div className="card-body ">
<div className="flex w-full gap-4">
<Button
isLoading={loading}
type="submit"
className="btn btn-primary flex-1"
>
Speichern
</Button>
{event && (
<Button
isLoading={deleteLoading}
onClick={async () => {
setDeleteLoading(true);
await deleteEvent(event.id);
redirect("/admin/event");
}}
className="btn btn-error"
>
Löschen
</Button>
)}
</div>
</div>
</div>
</form>
</>
);
};

View File

@@ -1,14 +1,6 @@
import { prisma } from '@repo/db';
import { Form } from '../_components/Form';
import { prisma } from "@repo/db";
import { Form } from "../_components/Form";
export default async () => {
const users = await prisma.user.findMany({
select: {
id: true,
firstname: true,
lastname: true,
publicId: true,
},
});
return <Form users={users} />;
return <Form />;
};

View File

@@ -1,246 +1,245 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { StationOptionalDefaultsSchema } from '@repo/db/zod';
import { set, useForm } from 'react-hook-form';
import { z } from 'zod';
import { BosUse, Country, Station } from '@repo/db';
import { FileText, LocateIcon, PlaneIcon } from 'lucide-react';
import { Input } from '../../../../_components/ui/Input';
import { useState } from 'react';
import { deleteStation, upsertStation } from '../action';
import { Button } from '../../../../_components/ui/Button';
import { redirect } from 'next/navigation';
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { StationOptionalDefaultsSchema } from "@repo/db/zod";
import { set, useForm } from "react-hook-form";
import { z } from "zod";
import { BosUse, Country, Station } from "@repo/db";
import { FileText, LocateIcon, PlaneIcon } from "lucide-react";
import { Input } from "../../../../_components/ui/Input";
import { useState } from "react";
import { deleteStation, upsertStation } from "../action";
import { Button } from "../../../../_components/ui/Button";
import { redirect } from "next/navigation";
export const StationForm = ({ station }: { station?: Station }) => {
const form = useForm<z.infer<typeof StationOptionalDefaultsSchema>>({
resolver: zodResolver(StationOptionalDefaultsSchema),
defaultValues: station,
});
const [loading, setLoading] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
console.log(form.formState.errors);
return (
<>
<form
onSubmit={form.handleSubmit(async (values) => {
setLoading(true);
const createdStation = await upsertStation(values, station?.id);
setLoading(false);
if (!station) redirect(`/admin/station`);
})}
className="grid grid-cols-6 gap-3"
>
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
<div className="card-body">
<h2 className="card-title">
<FileText className="w-5 h-5" /> Allgemeines
</h2>
<Input
form={form}
label="BOS Rufname"
name="bosCallsign"
className="input-sm"
/>
<Input
form={form}
label="BOS Rufname (kurz)"
name="bosCallsignShort"
className="input-sm"
/>
<Input
form={form}
label="Betreiber"
name="operator"
className="input-sm"
/>
<Input
form={form}
label="ATC Rufname"
name="atcCallsign"
className="input-sm"
/>
<Input
form={form}
label="FIR (Flight Information Region)"
name="fir"
className="input-sm"
/>
<Input
form={form}
label="Leitstelle Rufname"
name="bosRadioArea"
className="input-sm"
/>
const form = useForm<z.infer<typeof StationOptionalDefaultsSchema>>({
resolver: zodResolver(StationOptionalDefaultsSchema),
defaultValues: station,
});
const [loading, setLoading] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
return (
<>
<form
onSubmit={form.handleSubmit(async (values) => {
setLoading(true);
const createdStation = await upsertStation(values, station?.id);
setLoading(false);
if (!station) redirect(`/admin/station`);
})}
className="grid grid-cols-6 gap-3"
>
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
<div className="card-body">
<h2 className="card-title">
<FileText className="w-5 h-5" /> Allgemeines
</h2>
<Input
form={form}
label="BOS Rufname"
name="bosCallsign"
className="input-sm"
/>
<Input
form={form}
label="BOS Rufname (kurz)"
name="bosCallsignShort"
className="input-sm"
/>
<Input
form={form}
label="Betreiber"
name="operator"
className="input-sm"
/>
<Input
form={form}
label="ATC Rufname"
name="atcCallsign"
className="input-sm"
/>
<Input
form={form}
label="FIR (Flight Information Region)"
name="fir"
className="input-sm"
/>
<Input
form={form}
label="Leitstelle Rufname"
name="bosRadioArea"
className="input-sm"
/>
<label className="form-control w-full">
<span className="label-text text-lg flex items-center gap-2">
BOS Nutzung
</span>
<select
className="input-sm select select-bordered select-sm"
{...form.register('bosUse')}
>
{Object.keys(BosUse).map((use) => (
<option key={use} value={use}>
{use}
</option>
))}
</select>
</label>
</div>
</div>
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
<div className="card-body">
<h2 className="card-title">
<LocateIcon className="w-5 h-5" /> Standort + Ausrüstung
</h2>
<label className="form-control w-full">
<span className="label-text text-lg flex items-center gap-2">
Land
</span>
<select
className="input-sm select select-bordered select-sm"
{...form.register('country', {})}
>
{Object.keys(Country).map((use) => (
<option key={use} value={use}>
{use}
</option>
))}
</select>
</label>
<Input
form={form}
label="Bundesland"
name="locationState"
className="input-sm"
/>
<Input
form={form}
label="Bundesland (kurz)"
name="locationStateShort"
className="input-sm"
/>
<span className="label-text text-lg flex items-center gap-2">
Ausgerüstet mit:
</span>
<div className="form-control">
<label className="label cursor-pointer">
<span>Winde</span>
<input
type="checkbox"
className="toggle"
{...form.register('hasWinch')}
/>
</label>
<label className="label cursor-pointer">
<span>Nachtsicht Gerät</span>
<input
type="checkbox"
className="toggle"
{...form.register('hasNvg')}
/>
</label>
<label className="label cursor-pointer">
<span>24-Stunden Einsatzfähig</span>
<input
type="checkbox"
className="toggle"
{...form.register('is24h')}
/>
</label>
<label className="label cursor-pointer">
<span>Bergetau</span>
<input
type="checkbox"
className="toggle"
{...form.register('hasRope')}
/>
</label>
</div>
<label className="form-control w-full">
<span className="label-text text-lg flex items-center gap-2">
BOS Nutzung
</span>
<select
className="input-sm select select-bordered select-sm"
{...form.register("bosUse")}
>
{Object.keys(BosUse).map((use) => (
<option key={use} value={use}>
{use}
</option>
))}
</select>
</label>
</div>
</div>
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
<div className="card-body">
<h2 className="card-title">
<LocateIcon className="w-5 h-5" /> Standort + Ausrüstung
</h2>
<label className="form-control w-full">
<span className="label-text text-lg flex items-center gap-2">
Land
</span>
<select
className="input-sm select select-bordered select-sm"
{...form.register("country", {})}
>
{Object.keys(Country).map((use) => (
<option key={use} value={use}>
{use}
</option>
))}
</select>
</label>
<Input
form={form}
label="Bundesland"
name="locationState"
className="input-sm"
/>
<Input
form={form}
label="Bundesland (kurz)"
name="locationStateShort"
className="input-sm"
/>
<span className="label-text text-lg flex items-center gap-2">
Ausgerüstet mit:
</span>
<div className="form-control">
<label className="label cursor-pointer">
<span>Winde</span>
<input
type="checkbox"
className="toggle"
{...form.register("hasWinch")}
/>
</label>
<label className="label cursor-pointer">
<span>Nachtsicht Gerät</span>
<input
type="checkbox"
className="toggle"
{...form.register("hasNvg")}
/>
</label>
<label className="label cursor-pointer">
<span>24-Stunden Einsatzfähig</span>
<input
type="checkbox"
className="toggle"
{...form.register("is24h")}
/>
</label>
<label className="label cursor-pointer">
<span>Bergetau</span>
<input
type="checkbox"
className="toggle"
{...form.register("hasRope")}
/>
</label>
</div>
<Input
form={form}
label="Breitengrad"
name="latitude"
className="input-sm"
formOptions={{ valueAsNumber: true }}
type="number"
step="any"
/>
<Input
form={form}
label="Längengrad"
name="longitude"
className="input-sm"
formOptions={{ valueAsNumber: true }}
type="number"
step="any"
/>
<label className="label cursor-pointer">
<span className="text-lg">Reichweiten ausblenden</span>
<input
type="checkbox"
className="toggle"
{...form.register('hideRangeRings')}
/>
</label>
</div>
</div>
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
<div className="card-body">
<h2 className="card-title">
<PlaneIcon className="w-5 h-5" /> Hubschrauber
</h2>
<Input
form={form}
label="Hubschrauber Typ"
name="aircraft"
className="input-sm"
/>
<Input
form={form}
formOptions={{ valueAsNumber: true }}
type="number"
label="Hubschrauber Geschwindigkeit"
className="input-sm"
name="aircraftSpeed"
/>
<Input
form={form}
label="Hubschrauber Registrierung"
name="aircraftRegistration"
className="input-sm"
/>
</div>
</div>
<div className="card bg-base-200 shadow-xl col-span-6">
<div className="card-body ">
<div className="flex w-full gap-4">
<Button
isLoading={loading}
type="submit"
className="btn btn-primary flex-1"
>
Speichern
</Button>
{station && (
<Button
isLoading={deleteLoading}
onClick={async () => {
setDeleteLoading(true);
await deleteStation(station.id);
redirect('/admin/station');
}}
className="btn btn-error"
>
Löschen
</Button>
)}
</div>
</div>
</div>
</form>
</>
);
<Input
form={form}
label="Breitengrad"
name="latitude"
className="input-sm"
formOptions={{ valueAsNumber: true }}
type="number"
step="any"
/>
<Input
form={form}
label="Längengrad"
name="longitude"
className="input-sm"
formOptions={{ valueAsNumber: true }}
type="number"
step="any"
/>
<label className="label cursor-pointer">
<span className="text-lg">Reichweiten ausblenden</span>
<input
type="checkbox"
className="toggle"
{...form.register("hideRangeRings")}
/>
</label>
</div>
</div>
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
<div className="card-body">
<h2 className="card-title">
<PlaneIcon className="w-5 h-5" /> Hubschrauber
</h2>
<Input
form={form}
label="Hubschrauber Typ"
name="aircraft"
className="input-sm"
/>
<Input
form={form}
formOptions={{ valueAsNumber: true }}
type="number"
label="Hubschrauber Geschwindigkeit"
className="input-sm"
name="aircraftSpeed"
/>
<Input
form={form}
label="Hubschrauber Registrierung"
name="aircraftRegistration"
className="input-sm"
/>
</div>
</div>
<div className="card bg-base-200 shadow-xl col-span-6">
<div className="card-body ">
<div className="flex w-full gap-4">
<Button
isLoading={loading}
type="submit"
className="btn btn-primary flex-1"
>
Speichern
</Button>
{station && (
<Button
isLoading={deleteLoading}
onClick={async () => {
setDeleteLoading(true);
await deleteStation(station.id);
redirect("/admin/station");
}}
className="btn btn-error"
>
Löschen
</Button>
)}
</div>
</div>
</div>
</form>
</>
);
};

View File

@@ -1,22 +1,22 @@
import { PrismaClient } from '@repo/db';
import { PrismaClient } from "@repo/db";
export default async ({ params }: { params: Promise<{ id: string }> }) => {
const prisma = new PrismaClient();
const { id } = await params;
const prisma = new PrismaClient();
const { id } = await params;
const user = await prisma.user.findUnique({
where: {
id: id,
},
});
console.log(user);
return (
<div>
<h1>
{user?.firstname} {user?.lastname}
</h1>
<p>{user?.email}</p>
{/* TODO: Hier Nutzerdaten bearbeiten */}
</div>
);
const user = await prisma.user.findUnique({
where: {
id: id,
},
});
console.log(user);
return (
<div>
<h1>
{user?.firstname} {user?.lastname}
</h1>
<p>{user?.email}</p>
{/* TODO: Hier Nutzerdaten bearbeiten */}
</div>
);
};

View File

@@ -1,107 +1,107 @@
'use client';
import { DrawingPinFilledIcon, EnterIcon } from '@radix-ui/react-icons';
import { Event, User } from '@repo/db';
import ModalBtn from './modalBtn';
import MDEditor from '@uiw/react-md-editor';
"use client";
import { DrawingPinFilledIcon, EnterIcon } from "@radix-ui/react-icons";
import { Event, User } from "@repo/db";
import ModalBtn from "./modalBtn";
import MDEditor from "@uiw/react-md-editor";
export const KursItem = ({ user, event }: { user: User; event: Event }) => {
return (
<div className="col-span-full">
<div className="card bg-base-200 shadow-xl mb-4">
<div className="card-body">
<h2 className="card-title">{event.name}</h2>
<div className="absolute top-0 right-0 m-4">
<span className="badge badge-info badge-outline">
Zusatzqualifikation
</span>
</div>
<div className="grid grid-cols-6 gap-4">
<div className="col-span-4">
<div className="text-left text-balance">
<MDEditor.Markdown
source={event.description}
className="whitespace-pre-wrap"
style={{
backgroundColor: 'transparent',
}}
/>
</div>
</div>
<div className="col-span-2">{event.finishedBadges}</div>
</div>
<div className="card-actions flex justify-between items-center mt-5">
<div>
<p className="text-gray-600 text-left flex items-center gap-2">
<DrawingPinFilledIcon /> <b>Teilnahmevoraussetzungen: </b>
{(!event.starterMoodleCourseId ||
!event.requiredBadges.length) &&
'Keine'}
{event.starterMoodleCourseId && (
<a className="link link-info" href="">
Moodle Kurs {event.starterMoodleCourseId}
</a>
)}
</p>
{!!event.requiredBadges.length && (
<div className="flex ml-6">
<b className="text-gray-600 text-left">Abzeichen:</b>
<div className="flex gap-2">
{event.requiredBadges.map((badge) => (
<div className="badge badge-secondary badge-outline">
{badge}
</div>
))}
</div>
</div>
)}
</div>
<ModalBtn
title={event.name}
dates={['Dienstag, 25 Februar 2025', 'Mittwoch, 26 Februar 2025']}
modalId={`${event.name}_modal.${event.id}`}
/>
</div>
</div>
</div>
</div>
);
return (
<div className="col-span-full">
<div className="card bg-base-200 shadow-xl mb-4">
<div className="card-body">
<h2 className="card-title">{event.name}</h2>
<div className="absolute top-0 right-0 m-4">
<span className="badge badge-info badge-outline">
Zusatzqualifikation
</span>
</div>
<div className="grid grid-cols-6 gap-4">
<div className="col-span-4">
<div className="text-left text-balance">
<MDEditor.Markdown
source={event.description}
className="whitespace-pre-wrap"
style={{
backgroundColor: "transparent",
}}
/>
</div>
</div>
<div className="col-span-2">{event.finishedBadges}</div>
</div>
<div className="card-actions flex justify-between items-center mt-5">
<div>
<p className="text-gray-600 text-left flex items-center gap-2">
<DrawingPinFilledIcon /> <b>Teilnahmevoraussetzungen: </b>
{(!event.starterMoodleCourseId ||
!event.requiredBadges.length) &&
"Keine"}
{event.starterMoodleCourseId && (
<a className="link link-info" href="">
Moodle Kurs {event.starterMoodleCourseId}
</a>
)}
</p>
{!!event.requiredBadges.length && (
<div className="flex ml-6">
<b className="text-gray-600 text-left">Abzeichen:</b>
<div className="flex gap-2">
{event.requiredBadges.map((badge) => (
<div className="badge badge-secondary badge-outline">
{badge}
</div>
))}
</div>
</div>
)}
</div>
<ModalBtn
title={event.name}
dates={["Dienstag, 25 Februar 2025", "Mittwoch, 26 Februar 2025"]}
modalId={`${event.name}_modal.${event.id}`}
/>
</div>
</div>
</div>
</div>
);
};
export const PilotKurs = ({ user }: { user: User }) => {
{
/* STATISCH, DA FÜR ALLE NEUEN MITGLIEDER MANDATORY, WIRD AUSGEBLENDET WENN ABSOLVIERT */
}
return (
<div className="col-span-full">
<div className="card card-bordered border-secondary bg-base-200 shadow-xl mb-4">
<div className="card-body">
<h2 className="card-title">Einsteigerkurs für Piloten</h2>
<div className="absolute top-0 right-0 m-4">
<span className="badge badge-secondary badge-outline">
Verpflichtend
</span>
</div>
<div className="grid grid-cols-6 gap-4">
<div className="col-span-4">
<p className="text-left text-balance">
In diesem Kurs lernen Piloten die Grundlagen der Luftrettung,
Einsatzverfahren, den Umgang mit dem BOS-Funk und einige
medizinische Basics. Der Kurs bietet eine ideale Vorbereitung
für alle Standard Operations bei Virtual Air Rescue.
</p>
</div>
<div className="col-span-2">Badge</div>
</div>
<div className="card-actions flex justify-between items-center mt-5">
<p className="text-gray-600 text-left flex items-center gap-2">
<DrawingPinFilledIcon /> <b>Teilnahmevoraussetzungen:</b> Keine
</p>
<button className="btn btn-outline btn-secondary btn-wide">
<EnterIcon /> Zum Moodle Kurs
</button>
</div>
</div>
</div>
</div>
);
{
/* STATISCH, DA FÜR ALLE NEUEN MITGLIEDER MANDATORY, WIRD AUSGEBLENDET WENN ABSOLVIERT */
}
return (
<div className="col-span-full">
<div className="card card-bordered border-secondary bg-base-200 shadow-xl mb-4">
<div className="card-body">
<h2 className="card-title">Einsteigerkurs für Piloten</h2>
<div className="absolute top-0 right-0 m-4">
<span className="badge badge-secondary badge-outline">
Verpflichtend
</span>
</div>
<div className="grid grid-cols-6 gap-4">
<div className="col-span-4">
<p className="text-left text-balance">
In diesem Kurs lernen Piloten die Grundlagen der Luftrettung,
Einsatzverfahren, den Umgang mit dem BOS-Funk und einige
medizinische Basics. Der Kurs bietet eine ideale Vorbereitung
für alle Standard Operations bei Virtual Air Rescue.
</p>
</div>
<div className="col-span-2">Badge</div>
</div>
<div className="card-actions flex justify-between items-center mt-5">
<p className="text-gray-600 text-left flex items-center gap-2">
<DrawingPinFilledIcon /> <b>Teilnahmevoraussetzungen:</b> Keine
</p>
<button className="btn btn-outline btn-secondary btn-wide">
<EnterIcon /> Zum Moodle Kurs
</button>
</div>
</div>
</div>
</div>
);
};

View File

@@ -1,8 +1,8 @@
import type { Metadata } from "next";
import {
DiscordLogoIcon,
InstagramLogoIcon,
ReaderIcon,
DiscordLogoIcon,
InstagramLogoIcon,
ReaderIcon,
} from "@radix-ui/react-icons";
import { HorizontalNav, VerticalNav } from "../_components/Nav";
import { Toaster } from "react-hot-toast";
@@ -11,95 +11,95 @@ import { getServerSession } from "../api/auth/[...nextauth]/auth";
import { headers } from "next/headers";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Create Next App",
description: "Generated by create next app",
};
export default async function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode;
}>) {
const session = await getServerSession();
const session = await getServerSession();
if (!session) redirect(`/login`);
if (!session) redirect(`/login`);
return (
<div
className="hero min-h-screen"
style={{
backgroundImage: "url('/bg.png')",
}}
>
<div className="hero-overlay bg-opacity-30"></div>
<div>
<Toaster position="top-center" reverseOrder={false} />
</div>
{/* Card */}
<div className="hero-content text-neutral-content text-center w-full max-w-full h-full m-10">
<div className="card bg-base-100 shadow-2xl w-full min-h-full h-full max-h-[calc(100vh-13rem)] p-4 flex flex-col mr-24 ml-24">
{/* Top Navbar */}
<HorizontalNav />
return (
<div
className="hero min-h-screen"
style={{
backgroundImage: "url('/bg.png')",
}}
>
<div className="hero-overlay bg-opacity-30"></div>
<div>
<Toaster position="top-center" reverseOrder={false} />
</div>
{/* Card */}
<div className="hero-content text-neutral-content text-center w-full max-w-full h-full m-10">
<div className="card bg-base-100 shadow-2xl w-full min-h-full h-full max-h-[calc(100vh-13rem)] p-4 flex flex-col mr-24 ml-24">
{/* Top Navbar */}
<HorizontalNav />
{/* Hauptlayout: Sidebar + Content (nimmt Resthöhe ein) */}
<div className="flex grow overflow-hidden">
{/* Linke Sidebar */}
<VerticalNav />
{/* Hauptlayout: Sidebar + Content (nimmt Resthöhe ein) */}
<div className="flex grow overflow-hidden">
{/* Linke Sidebar */}
<VerticalNav />
{/* Scrollbarer Content-Bereich */}
<div className="flex-grow bg-base-100 p-6 rounded-lg shadow-md ml-4 overflow-auto h-full max-w-full w-full">
{children}
</div>
</div>
{/* Scrollbarer Content-Bereich */}
<div className="flex-grow bg-base-100 p-6 rounded-lg shadow-md ml-4 overflow-auto h-full max-w-full w-full">
{children}
</div>
</div>
{/* Footer */}
<footer className="footer flex justify-between items-center p-4 bg-base-200 mt-4 rounded-lg shadow-md">
{/* Left: Impressum & Datenschutz */}
<div className="flex gap-4 text-sm">
<a href="/impressum" className="hover:text-primary">
Impressum
</a>
<a href="/datenschutz" className="hover:text-primary">
Datenschutzerklärung
</a>
</div>
{/* Footer */}
<footer className="footer flex justify-between items-center p-4 bg-base-200 mt-4 rounded-lg shadow-md">
{/* Left: Impressum & Datenschutz */}
<div className="flex gap-4 text-sm">
<a href="/impressum" className="hover:text-primary">
Impressum
</a>
<a href="/datenschutz" className="hover:text-primary">
Datenschutzerklärung
</a>
</div>
{/* Center: Copyright */}
<p className="text-sm">
Copyright © {new Date().getFullYear()} - Virtual Air Rescue
</p>
{/* Center: Copyright */}
<p className="text-sm">
Copyright © {new Date().getFullYear()} - Virtual Air Rescue
</p>
{/* Right: Social Icons */}
<div className="flex gap-4">
<div className="tooltip tooltip-top" data-tip="Discord">
<a
href="https://discord.com"
target="_blank"
rel="noopener noreferrer"
className="hover:text-primary"
>
<DiscordLogoIcon className="w-5 h-5" />
</a>
</div>
<div className="tooltip tooltip-top" data-tip="Instagram">
<a
href="https://instagram.com"
target="_blank"
rel="noopener noreferrer"
className="hover:text-primary"
>
<InstagramLogoIcon className="w-5 h-5" />
</a>
</div>
<div className="tooltip tooltip-top" data-tip="Knowledgebase">
<a href="/docs" className="hover:text-primary">
<ReaderIcon className="w-5 h-5" />
</a>
</div>
</div>
</footer>
</div>
</div>
</div>
);
{/* Right: Social Icons */}
<div className="flex gap-4">
<div className="tooltip tooltip-top" data-tip="Discord">
<a
href="https://discord.com"
target="_blank"
rel="noopener noreferrer"
className="hover:text-primary"
>
<DiscordLogoIcon className="w-5 h-5" />
</a>
</div>
<div className="tooltip tooltip-top" data-tip="Instagram">
<a
href="https://instagram.com"
target="_blank"
rel="noopener noreferrer"
className="hover:text-primary"
>
<InstagramLogoIcon className="w-5 h-5" />
</a>
</div>
<div className="tooltip tooltip-top" data-tip="Knowledgebase">
<a href="/docs" className="hover:text-primary">
<ReaderIcon className="w-5 h-5" />
</a>
</div>
</div>
</footer>
</div>
</div>
</div>
);
}

View File

@@ -15,21 +15,21 @@ Aktive Events / Mandatory Events
*/
export default function Home() {
const [isChecked, setIsChecked] = useState(true);
const [isChecked, setIsChecked] = useState(true);
const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsChecked(event.target.checked);
};
const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsChecked(event.target.checked);
};
return (
<div>
<Header
isChecked={isChecked}
handleCheckboxChange={handleCheckboxChange}
/>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<Stats isChecked={isChecked} />
</div>
</div>
);
return (
<div>
<Header
isChecked={isChecked}
handleCheckboxChange={handleCheckboxChange}
/>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<Stats isChecked={isChecked} />
</div>
</div>
);
}

View File

@@ -10,339 +10,338 @@ import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { Button } from "../../../_components/ui/Button";
import {
PersonIcon,
EnvelopeClosedIcon,
BookmarkIcon,
DiscordLogoIcon,
PaperPlaneIcon,
Link2Icon,
MixerHorizontalIcon,
LockClosedIcon,
LockOpen2Icon,
LockOpen1Icon,
PersonIcon,
EnvelopeClosedIcon,
BookmarkIcon,
DiscordLogoIcon,
PaperPlaneIcon,
Link2Icon,
MixerHorizontalIcon,
LockClosedIcon,
LockOpen2Icon,
LockOpen1Icon,
} from "@radix-ui/react-icons";
export const ProfileForm = ({ user }: { user: User }) => {
const schema = z.object({
firstname: z.string().min(2).max(30),
lastname: z.string().min(2).max(30),
email: z.string().email({
message: "Bitte gebe eine gültige E-Mail Adresse ein",
}),
});
const [isLoading, setIsLoading] = useState(false);
type IFormInput = z.infer<typeof schema>;
const schema = z.object({
firstname: z.string().min(2).max(30),
lastname: z.string().min(2).max(30),
email: z.string().email({
message: "Bitte gebe eine gültige E-Mail Adresse ein",
}),
});
const [isLoading, setIsLoading] = useState(false);
type IFormInput = z.infer<typeof schema>;
const form = useForm<IFormInput>({
defaultValues: {
firstname: user.firstname,
lastname: user.lastname,
email: user.email,
},
resolver: zodResolver(schema),
});
return (
<form
className="card-body"
onSubmit={form.handleSubmit(async (values) => {
setIsLoading(true);
await updateUser(values);
form.reset(values);
setIsLoading(false);
toast.success("Deine Änderungen wurden gespeichert!", {
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
});
})}
>
<h2 className="card-title">
<MixerHorizontalIcon className="w-5 h-5" /> Persönliche Informationen
</h2>
<div className="text-left">
<label className="floating-label w-full mb-5 mt-5">
<span className="text-lg flex items-center gap-2">
<PersonIcon /> Vorname
</span>
<input
{...form.register("firstname")}
type="text"
className="input input-bordered w-full"
defaultValue={user.firstname}
placeholder="Vorname"
/>
</label>
{form.formState.errors.firstname && (
<p className="text-error">
{form.formState.errors.firstname.message}
</p>
)}
<label className="floating-label w-full mb-5">
<span className="text-lg flex items-center gap-2">
<PersonIcon /> Nachname
</span>
<input
{...form.register("lastname")}
type="text"
className="input input-bordered w-full"
defaultValue={user.lastname}
placeholder="Nachname"
/>
</label>
{form.formState.errors.lastname && (
<p className="text-error">
{form.formState.errors.lastname?.message}
</p>
)}
<label className="floating-label w-full">
<span className="text-lg flex items-center gap-2">
<EnvelopeClosedIcon /> E-Mail
</span>
<input
{...form.register("email")}
type="text"
className="input input-bordered w-full"
defaultValue={user.email}
placeholder="E-Mail"
/>
</label>
{form.formState.errors.email && (
<p className="text-error">{form.formState.errors.email?.message}</p>
)}
<div className="card-actions justify-center pt-6">
<Button
role="submit"
className="btn-sm btn-wide btn-outline btn-primary"
disabled={!form.formState.isDirty}
isLoading={isLoading}
>
<BookmarkIcon /> Speichern
</Button>
</div>
</div>
</form>
);
const form = useForm<IFormInput>({
defaultValues: {
firstname: user.firstname,
lastname: user.lastname,
email: user.email,
},
resolver: zodResolver(schema),
});
return (
<form
className="card-body"
onSubmit={form.handleSubmit(async (values) => {
setIsLoading(true);
await updateUser(values);
form.reset(values);
setIsLoading(false);
toast.success("Deine Änderungen wurden gespeichert!", {
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
});
})}
>
<h2 className="card-title">
<MixerHorizontalIcon className="w-5 h-5" /> Persönliche Informationen
</h2>
<div className="text-left">
<label className="floating-label w-full mb-5 mt-5">
<span className="text-lg flex items-center gap-2">
<PersonIcon /> Vorname
</span>
<input
{...form.register("firstname")}
type="text"
className="input input-bordered w-full"
defaultValue={user.firstname}
placeholder="Vorname"
/>
</label>
{form.formState.errors.firstname && (
<p className="text-error">
{form.formState.errors.firstname.message}
</p>
)}
<label className="floating-label w-full mb-5">
<span className="text-lg flex items-center gap-2">
<PersonIcon /> Nachname
</span>
<input
{...form.register("lastname")}
type="text"
className="input input-bordered w-full"
defaultValue={user.lastname}
placeholder="Nachname"
/>
</label>
{form.formState.errors.lastname && (
<p className="text-error">
{form.formState.errors.lastname?.message}
</p>
)}
<label className="floating-label w-full">
<span className="text-lg flex items-center gap-2">
<EnvelopeClosedIcon /> E-Mail
</span>
<input
{...form.register("email")}
type="text"
className="input input-bordered w-full"
defaultValue={user.email}
placeholder="E-Mail"
/>
</label>
{form.formState.errors.email && (
<p className="text-error">{form.formState.errors.email?.message}</p>
)}
<div className="card-actions justify-center pt-6">
<Button
role="submit"
className="btn-sm btn-wide btn-outline btn-primary"
disabled={!form.formState.isDirty}
isLoading={isLoading}
>
<BookmarkIcon /> Speichern
</Button>
</div>
</div>
</form>
);
};
export const SocialForm = ({
discordAccount,
user,
discordAccount,
user,
}: {
discordAccount?: DiscordAccount;
user: User;
discordAccount?: DiscordAccount;
user: User;
}) => {
const [isLoading, setIsLoading] = useState(false);
const [vatsimLoading, setVatsimLoading] = useState(false);
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [vatsimLoading, setVatsimLoading] = useState(false);
const router = useRouter();
const schema = z.object({
vatsimCid: z.number().min(1000).max(9999999),
});
const schema = z.object({
vatsimCid: z.number().min(1000).max(9999999),
});
type IFormInput = z.infer<typeof schema>;
type IFormInput = z.infer<typeof schema>;
const form = useForm<IFormInput>({
defaultValues: {
vatsimCid: user?.vatsimCid || undefined,
},
resolver: zodResolver(schema),
});
const form = useForm<IFormInput>({
defaultValues: {
vatsimCid: user?.vatsimCid || undefined,
},
resolver: zodResolver(schema),
});
if (!user) return null;
console.log("Dirty", form.formState.isDirty);
return (
<form
className="card-body"
onSubmit={form.handleSubmit(async (values) => {
form.handleSubmit(async () => {
setVatsimLoading(true);
});
await updateUser({
vatsimCid: values.vatsimCid,
});
setVatsimLoading(false);
form.reset(values);
toast.success("Deine Änderungen wurden gespeichert!", {
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
});
})}
>
<h2 className="card-title mb-5">
<Link2Icon className="w-5 h-5" /> Verbindungen & Benachrichtigungen
</h2>
<div>
<div>
{discordAccount ? (
<Button
className="btn-success btn-block btn-outline group transition-all duration-0 hover:btn-error"
isLoading={isLoading}
onClick={async () => {
setIsLoading(true);
await unlinkDiscord(user.id);
router.refresh();
setIsLoading(false);
}}
>
<DiscordLogoIcon className="w-5 h-5" />
<span className="group-hover:hidden">
Verbunden mit {discordAccount.username}
</span>
<span className="hidden group-hover:inline">
Verbindung trennen{isLoading && "..."}
</span>
</Button>
) : (
<a href={process.env.NEXT_PUBLIC_DISCORD_URL}>
<button
type="button"
className="btn btn-primary btn-block"
onSubmit={() => false}
>
<DiscordLogoIcon className="w-5 h-5" /> Mit Discord verbinden
</button>
</a>
)}
</div>
</div>
<div className="content-center">
<label className="floating-label w-full mt-5">
<span className="text-lg flex items-center gap-2">
<PaperPlaneIcon /> VATSIM-CID
</span>
<input
type="number"
className="input input-bordered w-full"
placeholder="1445241"
defaultValue={user.vatsimCid as number | undefined}
{...form.register("vatsimCid", {
valueAsNumber: true,
})}
/>
</label>
{form.formState.errors.vatsimCid && (
<p className="text-error">
{form.formState.errors.vatsimCid.message}
</p>
)}
</div>
<div className="card-actions justify-center pt-6 mt-auto">
<Button
className="btn-sm btn-wide btn-outline btn-primary"
isLoading={vatsimLoading}
disabled={!form.formState.isDirty}
role="submit"
>
<BookmarkIcon /> Speichern
</Button>
</div>
</form>
);
if (!user) return null;
return (
<form
className="card-body"
onSubmit={form.handleSubmit(async (values) => {
form.handleSubmit(async () => {
setVatsimLoading(true);
});
await updateUser({
vatsimCid: values.vatsimCid,
});
setVatsimLoading(false);
form.reset(values);
toast.success("Deine Änderungen wurden gespeichert!", {
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
});
})}
>
<h2 className="card-title mb-5">
<Link2Icon className="w-5 h-5" /> Verbindungen & Benachrichtigungen
</h2>
<div>
<div>
{discordAccount ? (
<Button
className="btn-success btn-block btn-outline group transition-all duration-0 hover:btn-error"
isLoading={isLoading}
onClick={async () => {
setIsLoading(true);
await unlinkDiscord(user.id);
router.refresh();
setIsLoading(false);
}}
>
<DiscordLogoIcon className="w-5 h-5" />
<span className="group-hover:hidden">
Verbunden mit {discordAccount.username}
</span>
<span className="hidden group-hover:inline">
Verbindung trennen{isLoading && "..."}
</span>
</Button>
) : (
<a href={process.env.NEXT_PUBLIC_DISCORD_URL}>
<button
type="button"
className="btn btn-primary btn-block"
onSubmit={() => false}
>
<DiscordLogoIcon className="w-5 h-5" /> Mit Discord verbinden
</button>
</a>
)}
</div>
</div>
<div className="content-center">
<label className="floating-label w-full mt-5">
<span className="text-lg flex items-center gap-2">
<PaperPlaneIcon /> VATSIM-CID
</span>
<input
type="number"
className="input input-bordered w-full"
placeholder="1445241"
defaultValue={user.vatsimCid as number | undefined}
{...form.register("vatsimCid", {
valueAsNumber: true,
})}
/>
</label>
{form.formState.errors.vatsimCid && (
<p className="text-error">
{form.formState.errors.vatsimCid.message}
</p>
)}
</div>
<div className="card-actions justify-center pt-6 mt-auto">
<Button
className="btn-sm btn-wide btn-outline btn-primary"
isLoading={vatsimLoading}
disabled={!form.formState.isDirty}
role="submit"
>
<BookmarkIcon /> Speichern
</Button>
</div>
</form>
);
};
export const PasswordForm = ({ user }: { user: User }) => {
const schema = z.object({
password: z.string().min(2).max(30),
newPassword: z.string().min(2).max(30),
newPasswordConfirm: z.string().min(2).max(30),
});
const [isLoading, setIsLoading] = useState(false);
type IFormInput = z.infer<typeof schema>;
const schema = z.object({
password: z.string().min(2).max(30),
newPassword: z.string().min(2).max(30),
newPasswordConfirm: z.string().min(2).max(30),
});
const [isLoading, setIsLoading] = useState(false);
type IFormInput = z.infer<typeof schema>;
const form = useForm<IFormInput>({
defaultValues: {},
resolver: zodResolver(schema),
});
return (
<form
className="card-body"
onSubmit={form.handleSubmit(async (values) => {
setIsLoading(true);
const result = await changePassword(
values.password,
values.newPassword
);
form.reset(values);
setIsLoading(false);
if (result.error) {
form.setError("password", {
type: "manual",
message: result.error,
});
} else if (result.success) {
toast.success("Dein Passwort wurde geändert!", {
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
});
}
})}
>
<h2 className="card-title">
<LockClosedIcon className="w-5 h-5" /> Password Ändern
</h2>
<div className="">
<label className="floating-label w-full mt-5 mb-5">
<span className="text-lg flex items-center gap-2">
<LockOpen2Icon /> Aktuelles Passwort
</span>
<input
{...form.register("password")}
type="text"
className="input input-bordered w-full"
defaultValue={""}
/>
</label>
{form.formState.errors.password && (
<p className="text-error">{form.formState.errors.password.message}</p>
)}
<label className="floating-label w-full mb-5">
<span className="text-lg flex items-center gap-2">
<LockOpen1Icon /> Neues Passwort
</span>
<input
{...form.register("newPassword")}
type="text"
className="input input-bordered w-full"
defaultValue={""}
/>
</label>
{form.formState.errors.newPassword && (
<p className="text-error">
{form.formState.errors.newPassword?.message}
</p>
)}
<label className="floating-label w-full">
<span className="text-lg flex items-center gap-2">
<LockOpen1Icon /> Passwort wiederholen
</span>
<input
{...form.register("newPasswordConfirm")}
type="text"
className="input input-bordered w-full"
defaultValue={""}
/>
</label>
{form.formState.errors.newPasswordConfirm && (
<p className="text-error">
{form.formState.errors.newPasswordConfirm?.message}
</p>
)}
<div className="card-actions justify-center pt-6">
<Button
role="submit"
className="btn-sm btn-wide btn-outline btn-primary"
disabled={!form.formState.isDirty}
isLoading={isLoading}
>
<BookmarkIcon /> Speichern
</Button>
</div>
</div>
</form>
);
const form = useForm<IFormInput>({
defaultValues: {},
resolver: zodResolver(schema),
});
return (
<form
className="card-body"
onSubmit={form.handleSubmit(async (values) => {
setIsLoading(true);
const result = await changePassword(
values.password,
values.newPassword,
);
form.reset(values);
setIsLoading(false);
if (result.error) {
form.setError("password", {
type: "manual",
message: result.error,
});
} else if (result.success) {
toast.success("Dein Passwort wurde geändert!", {
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
});
}
})}
>
<h2 className="card-title">
<LockClosedIcon className="w-5 h-5" /> Password Ändern
</h2>
<div className="">
<label className="floating-label w-full mt-5 mb-5">
<span className="text-lg flex items-center gap-2">
<LockOpen2Icon /> Aktuelles Passwort
</span>
<input
{...form.register("password")}
type="text"
className="input input-bordered w-full"
defaultValue={""}
/>
</label>
{form.formState.errors.password && (
<p className="text-error">{form.formState.errors.password.message}</p>
)}
<label className="floating-label w-full mb-5">
<span className="text-lg flex items-center gap-2">
<LockOpen1Icon /> Neues Passwort
</span>
<input
{...form.register("newPassword")}
type="text"
className="input input-bordered w-full"
defaultValue={""}
/>
</label>
{form.formState.errors.newPassword && (
<p className="text-error">
{form.formState.errors.newPassword?.message}
</p>
)}
<label className="floating-label w-full">
<span className="text-lg flex items-center gap-2">
<LockOpen1Icon /> Passwort wiederholen
</span>
<input
{...form.register("newPasswordConfirm")}
type="text"
className="input input-bordered w-full"
defaultValue={""}
/>
</label>
{form.formState.errors.newPasswordConfirm && (
<p className="text-error">
{form.formState.errors.newPasswordConfirm?.message}
</p>
)}
<div className="card-actions justify-center pt-6">
<Button
role="submit"
className="btn-sm btn-wide btn-outline btn-primary"
disabled={!form.formState.isDirty}
isLoading={isLoading}
>
<BookmarkIcon /> Speichern
</Button>
</div>
</div>
</form>
);
};