added event helpers to ui libary, added Badge component, reordered Dashboard Components, splitted Event Admin page
12
.vscode/settings.json
vendored
@@ -19,5 +19,17 @@
|
|||||||
},
|
},
|
||||||
"[xml]": {
|
"[xml]": {
|
||||||
"editor.defaultFormatter": "redhat.vscode-xml"
|
"editor.defaultFormatter": "redhat.vscode-xml"
|
||||||
|
},
|
||||||
|
"sqltools.connections": [
|
||||||
|
{
|
||||||
|
"previewLimit": 50,
|
||||||
|
"server": "localhost",
|
||||||
|
"port": 5432,
|
||||||
|
"driver": "PostgreSQL",
|
||||||
|
"name": "Persistant-Data",
|
||||||
|
"database": "var",
|
||||||
|
"username": "persistant-data",
|
||||||
|
"password": "persistant-data-pw"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
1
Persistant-Data.session.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
SELECT * FROM users
|
||||||
28
apps/hub/app/(app)/_components/Badges.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Badge } from "@repo/ui";
|
||||||
|
|
||||||
|
import { Award } from "lucide-react";
|
||||||
|
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
|
||||||
|
|
||||||
|
export const Badges = async () => {
|
||||||
|
const session = await getServerSession();
|
||||||
|
if (!session) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title justify-between">
|
||||||
|
<span className="card-title">
|
||||||
|
<Award className="w-4 h-4" /> Verdiente Abzeichen
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
{session.user.badges.map((badge) => {
|
||||||
|
return (
|
||||||
|
<div className="badge badge-primary badge-outline">
|
||||||
|
<Badge name={badge} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
|
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
|
||||||
import { PrismaClient } from "@repo/db";
|
import { PrismaClient } from "@repo/db";
|
||||||
import { KursItem } from "../events/_components/item";
|
import { KursItem } from "../events/_components/item";
|
||||||
|
import { RocketIcon } from "lucide-react";
|
||||||
|
import { eventCompleted } from "@repo/ui";
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
@@ -39,9 +41,27 @@ export default async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filteredEvents = events.filter((event) => {
|
||||||
|
console.log;
|
||||||
|
if (eventCompleted(event, event.participants[0])) return false;
|
||||||
|
if (
|
||||||
|
event.type === "OBLIGATED_COURSE" &&
|
||||||
|
!eventCompleted(event, event.participants[0])
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!filteredEvents.length) return null;
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="col-span-full">
|
||||||
|
<p className="text-xl font-semibold text-left flex items-center gap-2 mb-2 mt-5">
|
||||||
|
<RocketIcon className="w-4 h-4" /> Laufende Events & Kurse
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-6 gap-4">
|
<div className="grid grid-cols-6 gap-4">
|
||||||
{events.map((event) => {
|
{filteredEvents.map((event) => {
|
||||||
return (
|
return (
|
||||||
<KursItem
|
<KursItem
|
||||||
selectedAppointments={userAppointments}
|
selectedAppointments={userAppointments}
|
||||||
@@ -52,5 +72,6 @@ export default async () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
203
apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import { DateInput } from "../../../../_components/ui/DateInput";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Event, Participant, Prisma } from "@repo/db";
|
||||||
|
import {
|
||||||
|
EventAppointmentOptionalDefaults,
|
||||||
|
EventAppointmentOptionalDefaultsSchema,
|
||||||
|
ParticipantOptionalDefaultsSchema,
|
||||||
|
} from "@repo/db/zod";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { deleteAppoinement, upsertAppointment } from "../action";
|
||||||
|
import { Button } from "../../../../_components/ui/Button";
|
||||||
|
import {
|
||||||
|
PaginatedTable,
|
||||||
|
PaginatedTableRef,
|
||||||
|
} from "../../../../_components/PaginatedTable";
|
||||||
|
import { Ref, RefObject, useRef } from "react";
|
||||||
|
import { CellContext } from "@tanstack/react-table";
|
||||||
|
import { upsertParticipant } from "../../../events/actions";
|
||||||
|
import { Switch } from "../../../../_components/ui/Switch";
|
||||||
|
|
||||||
|
interface AppointmentModalProps {
|
||||||
|
event?: Event;
|
||||||
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
|
appointmentsTableRef: React.RefObject<PaginatedTableRef>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppointmentModal = ({
|
||||||
|
event,
|
||||||
|
ref,
|
||||||
|
appointmentsTableRef,
|
||||||
|
}: AppointmentModalProps) => {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const appointmentForm = useForm<EventAppointmentOptionalDefaults>({
|
||||||
|
resolver: zodResolver(EventAppointmentOptionalDefaultsSchema),
|
||||||
|
defaultValues: {
|
||||||
|
eventId: event?.id,
|
||||||
|
presenterId: session?.user?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const participantTableRef = useRef<PaginatedTableRef>(null);
|
||||||
|
const participantForm = useForm<Participant>({
|
||||||
|
resolver: zodResolver(ParticipantOptionalDefaultsSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog ref={ref} 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 {appointmentForm.watch("id")}
|
||||||
|
</h3>
|
||||||
|
<form
|
||||||
|
onSubmit={appointmentForm.handleSubmit(async (values) => {
|
||||||
|
if (!event) return;
|
||||||
|
const createdAppointment = await upsertAppointment(values);
|
||||||
|
ref.current?.close();
|
||||||
|
appointmentsTableRef.current?.refresh();
|
||||||
|
})}
|
||||||
|
className="flex flex-col"
|
||||||
|
>
|
||||||
|
<DateInput control={appointmentForm.control} name="appointmentDate" />
|
||||||
|
{/* <Input
|
||||||
|
form={appointmentForm}
|
||||||
|
type="datetime-local"
|
||||||
|
label="Datum"
|
||||||
|
name="appointmentDate"
|
||||||
|
formOptions={{
|
||||||
|
valueAsDate: true,
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
<div>
|
||||||
|
{appointmentForm.watch("id") && (
|
||||||
|
<PaginatedTable
|
||||||
|
ref={participantTableRef}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
accessorKey: "User.firstname",
|
||||||
|
header: "Vorname",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "User.lastname",
|
||||||
|
header: "Nachname",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Aktion",
|
||||||
|
cell: ({ row }: CellContext<Participant, any>) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
participantForm.reset(row.original);
|
||||||
|
}}
|
||||||
|
className="btn btn-outline btn-sm"
|
||||||
|
>
|
||||||
|
anzeigen
|
||||||
|
</button>
|
||||||
|
{!row.original.attended &&
|
||||||
|
event?.hasPresenceEvents && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onSubmit={() => {}}
|
||||||
|
onClick={async () => {
|
||||||
|
await upsertParticipant({
|
||||||
|
eventId: event!.id,
|
||||||
|
userId: participantForm.watch("userId"),
|
||||||
|
attended: true,
|
||||||
|
});
|
||||||
|
participantTableRef.current?.refresh();
|
||||||
|
}}
|
||||||
|
className="btn btn-outline btn-info btn-sm"
|
||||||
|
>
|
||||||
|
Anwesend
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
prismaModel={"participant"}
|
||||||
|
filter={{
|
||||||
|
eventAppointmentId: appointmentForm.watch("id"),
|
||||||
|
}}
|
||||||
|
include={{ User: true }}
|
||||||
|
leftOfPagination={
|
||||||
|
<div className="space-x-1">
|
||||||
|
<Button type="submit" className="btn btn-primary">
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
{appointmentForm.watch("id") && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onSubmit={() => {}}
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteAppoinement(appointmentForm.watch("id")!);
|
||||||
|
ref.current?.close();
|
||||||
|
appointmentsTableRef.current?.refresh();
|
||||||
|
}}
|
||||||
|
className="btn btn-error btn-outline"
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="modal-action"></div>
|
||||||
|
</form>
|
||||||
|
{participantForm.watch("id") && (
|
||||||
|
<form
|
||||||
|
onSubmit={participantForm.handleSubmit(async (data) => {
|
||||||
|
await upsertParticipant({
|
||||||
|
...data,
|
||||||
|
statusLog: data.statusLog as Prisma.InputJsonValue[],
|
||||||
|
});
|
||||||
|
participantTableRef.current?.refresh();
|
||||||
|
})}
|
||||||
|
className="space-y-1"
|
||||||
|
>
|
||||||
|
<h1 className="text-2xl">Teilnehmer bearbeiten</h1>
|
||||||
|
<Switch
|
||||||
|
form={participantForm}
|
||||||
|
name="appointmentCancelled"
|
||||||
|
label="Termin abgesagt"
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
form={participantForm}
|
||||||
|
name="finisherMoodleCurseCompleted"
|
||||||
|
label="Abschluss-Moodle kurs abgeschlossen"
|
||||||
|
/>
|
||||||
|
{event?.hasPresenceEvents && (
|
||||||
|
<Switch
|
||||||
|
form={participantForm}
|
||||||
|
name="attended"
|
||||||
|
label="An Presenstermin teilgenommen"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h3 className="text-xl">Verlauf</h3>
|
||||||
|
{participantForm.watch("statusLog").map((s) => {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<p>{(s as any).event}</p>
|
||||||
|
<p>{new Date((s as any).timestamp).toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<Button>Speichern</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -38,10 +38,7 @@ import {
|
|||||||
import { Select } from "../../../../_components/ui/Select";
|
import { Select } from "../../../../_components/ui/Select";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { MarkdownEditor } from "../../../../_components/ui/MDEditor";
|
import { MarkdownEditor } from "../../../../_components/ui/MDEditor";
|
||||||
import { CellContext } from "@tanstack/react-table";
|
import { AppointmentModal } from "./AppointmentModal";
|
||||||
import { upsertParticipant } from "../../../events/actions";
|
|
||||||
import { de } from "date-fns/locale";
|
|
||||||
registerLocale("de", de);
|
|
||||||
|
|
||||||
export const Form = ({ event }: { event?: Event }) => {
|
export const Form = ({ event }: { event?: Event }) => {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
@@ -66,177 +63,10 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<dialog ref={appointmentModal} className="modal">
|
<AppointmentModal
|
||||||
<div className="modal-box">
|
ref={appointmentModal}
|
||||||
<form method="dialog">
|
appointmentsTableRef={appointmentsTableRef}
|
||||||
{/* 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 {appointmentForm.watch("id")}
|
|
||||||
</h3>
|
|
||||||
<form
|
|
||||||
onSubmit={appointmentForm.handleSubmit(async (values) => {
|
|
||||||
if (!event) return;
|
|
||||||
const createdAppointment = await upsertAppointment(values);
|
|
||||||
appointmentModal.current?.close();
|
|
||||||
appointmentsTableRef.current?.refresh();
|
|
||||||
})}
|
|
||||||
className="flex flex-col"
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={appointmentForm.control}
|
|
||||||
name="appointmentDate"
|
|
||||||
render={({ field }) => (
|
|
||||||
<DatePicker
|
|
||||||
locale={"de"}
|
|
||||||
showTimeCaption
|
|
||||||
showTimeInput
|
|
||||||
showTimeSelect
|
|
||||||
placeholderText="Select date"
|
|
||||||
onChange={(date) => field.onChange(date)}
|
|
||||||
selected={field.value}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{/* <Input
|
|
||||||
form={appointmentForm}
|
|
||||||
type="datetime-local"
|
|
||||||
label="Datum"
|
|
||||||
name="appointmentDate"
|
|
||||||
formOptions={{
|
|
||||||
valueAsDate: true,
|
|
||||||
}}
|
|
||||||
/> */}
|
|
||||||
<div>
|
|
||||||
{appointmentForm.watch("id") && (
|
|
||||||
<PaginatedTable
|
|
||||||
ref={participantTableRef}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
accessorKey: "User.firstname",
|
|
||||||
header: "Vorname",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "User.lastname",
|
|
||||||
header: "Nachname",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Aktion",
|
|
||||||
cell: ({ row }: CellContext<Participant, any>) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
participantForm.reset(row.original);
|
|
||||||
}}
|
|
||||||
className="btn btn-outline btn-sm"
|
|
||||||
>
|
|
||||||
anzeigen
|
|
||||||
</button>
|
|
||||||
{!row.original.attended &&
|
|
||||||
event?.hasPresenceEvents && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onSubmit={() => {}}
|
|
||||||
onClick={async () => {
|
|
||||||
await upsertParticipant({
|
|
||||||
eventId: event!.id,
|
|
||||||
userId: participantForm.watch("userId"),
|
|
||||||
attended: true,
|
|
||||||
});
|
|
||||||
participantTableRef.current?.refresh();
|
|
||||||
}}
|
|
||||||
className="btn btn-outline btn-info btn-sm"
|
|
||||||
>
|
|
||||||
Anwesend
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
prismaModel={"participant"}
|
|
||||||
filter={{
|
|
||||||
eventAppointmentId: appointmentForm.watch("id"),
|
|
||||||
}}
|
|
||||||
include={{ User: true }}
|
|
||||||
leftOfPagination={
|
|
||||||
<div className="space-x-1">
|
|
||||||
<Button type="submit" className="btn btn-primary">
|
|
||||||
Speichern
|
|
||||||
</Button>
|
|
||||||
{appointmentForm.watch("id") && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onSubmit={() => {}}
|
|
||||||
onClick={async () => {
|
|
||||||
await deleteAppoinement(
|
|
||||||
appointmentForm.watch("id")!,
|
|
||||||
);
|
|
||||||
appointmentModal.current?.close();
|
|
||||||
appointmentsTableRef.current?.refresh();
|
|
||||||
}}
|
|
||||||
className="btn btn-error btn-outline"
|
|
||||||
>
|
|
||||||
Löschen
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="modal-action"></div>
|
|
||||||
</form>
|
|
||||||
{participantForm.watch("id") && (
|
|
||||||
<form
|
|
||||||
onSubmit={participantForm.handleSubmit(async (data) => {
|
|
||||||
await upsertParticipant({
|
|
||||||
...data,
|
|
||||||
statusLog: data.statusLog as Prisma.InputJsonValue[],
|
|
||||||
});
|
|
||||||
participantTableRef.current?.refresh();
|
|
||||||
})}
|
|
||||||
className="space-y-1"
|
|
||||||
>
|
|
||||||
<h1 className="text-2xl">Teilnehmer bearbeiten</h1>
|
|
||||||
<Switch
|
|
||||||
form={participantForm}
|
|
||||||
name="appointmentCancelled"
|
|
||||||
label="Termin abgesagt"
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
form={participantForm}
|
|
||||||
name="finisherMoodleCurseCompleted"
|
|
||||||
label="Abschluss-Moodle kurs abgeschlossen"
|
|
||||||
/>
|
|
||||||
{event?.hasPresenceEvents && (
|
|
||||||
<Switch
|
|
||||||
form={participantForm}
|
|
||||||
name="attended"
|
|
||||||
label="An Presenstermin teilgenommen"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<h3 className="text-xl">Verlauf</h3>
|
|
||||||
{participantForm.watch("statusLog").map((s) => {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<p>{(s as any).event}</p>
|
|
||||||
<p>{new Date((s as any).timestamp).toLocaleString()}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<Button>Speichern</Button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(async (values) => {
|
onSubmit={form.handleSubmit(async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -322,6 +152,7 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{form.watch("hasPresenceEvents") ? (
|
||||||
<div className="card bg-base-200 shadow-xl col-span-6">
|
<div className="card bg-base-200 shadow-xl col-span-6">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
@@ -408,6 +239,7 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="card bg-base-200 shadow-xl col-span-6">
|
<div className="card bg-base-200 shadow-xl col-span-6">
|
||||||
<div className="card-body ">
|
<div className="card-body ">
|
||||||
<div className="flex w-full gap-4">
|
<div className="flex w-full gap-4">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { Event, EventAppointment, Participant, prisma, User } from "@repo/db";
|
import { Event, EventAppointment, Participant, prisma, User } from "@repo/db";
|
||||||
import { cn } from "../../../../helper/cn";
|
import { cn } from "../../../../helper/cn";
|
||||||
import { inscribeToMoodleCourse, upsertParticipant } from "../actions";
|
import { inscribeToMoodleCourse, upsertParticipant } from "../actions";
|
||||||
import { Clock10Icon, Cross } from "lucide-react";
|
import { Check, Clock10Icon, Cross, EyeIcon } from "lucide-react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
EventAppointmentOptionalDefaults,
|
EventAppointmentOptionalDefaults,
|
||||||
@@ -22,6 +22,7 @@ import { Select } from "../../../_components/ui/Select";
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { JsonArray } from "../../../../../../packages/database/generated/client/runtime/library";
|
import { JsonArray } from "../../../../../../packages/database/generated/client/runtime/library";
|
||||||
|
import { eventCompleted } from "@repo/ui";
|
||||||
|
|
||||||
interface ModalBtnProps {
|
interface ModalBtnProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -90,10 +91,25 @@ const ModalBtn = ({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"btn btn-outline btn-info btn-wide",
|
"btn btn-outline btn-info btn-wide",
|
||||||
event.type === "OBLIGATED_COURSE" && "btn-secondary",
|
event.type === "OBLIGATED_COURSE" && "btn-secondary",
|
||||||
|
eventCompleted(event, participant) && "btn-success",
|
||||||
)}
|
)}
|
||||||
onClick={openModal}
|
onClick={openModal}
|
||||||
>
|
>
|
||||||
|
{participant && !eventCompleted(event, participant) && (
|
||||||
|
<>
|
||||||
|
<EyeIcon /> Anzeigen
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!participant && (
|
||||||
|
<>
|
||||||
<EnterIcon /> Anmelden
|
<EnterIcon /> Anmelden
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{eventCompleted(event, participant) && (
|
||||||
|
<>
|
||||||
|
<Check /> Abgeschlossen
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
<dialog id={modalId} className="modal">
|
<dialog id={modalId} className="modal">
|
||||||
<div className="modal-box">
|
<div className="modal-box">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ArrowRight, NotebookText, Award, RocketIcon } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Events from "./_components/Events";
|
import Events from "./_components/Events";
|
||||||
import StatsClientWrapper from "./_components/StatsClientWrapper";
|
import StatsClientWrapper from "./_components/StatsClientWrapper";
|
||||||
|
import { Badges } from "./_components/Badges";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
✔️ Einlog-Zeit
|
✔️ Einlog-Zeit
|
||||||
@@ -36,22 +37,7 @@ export default function Home() {
|
|||||||
<Logbook />
|
<Logbook />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
<Badges />
|
||||||
<div className="card-body">
|
|
||||||
<h2 className="card-title justify-between">
|
|
||||||
<span className="card-title">
|
|
||||||
<Award className="w-4 h-4" /> Verdiente Abzeichen
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
Badges
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-full">
|
|
||||||
<p className="text-xl font-semibold text-left flex items-center gap-2 mb-2 mt-5">
|
|
||||||
<RocketIcon className="w-4 h-4" /> Laufende Events & Kurse
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Events />
|
<Events />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
37
apps/hub/app/_components/ui/DateInput.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import DatePicker, { DatePickerProps, registerLocale } from "react-datepicker";
|
||||||
|
import {
|
||||||
|
Control,
|
||||||
|
Controller,
|
||||||
|
FieldValues,
|
||||||
|
Path,
|
||||||
|
PathValue,
|
||||||
|
} from "react-hook-form";
|
||||||
|
import { de } from "date-fns/locale";
|
||||||
|
registerLocale("de", de);
|
||||||
|
|
||||||
|
interface DateInputProps<T extends FieldValues>
|
||||||
|
extends Omit<DatePickerProps, "onChange" | "selected"> {
|
||||||
|
control: Control<T>;
|
||||||
|
name: Path<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DateInput = <T extends FieldValues>({
|
||||||
|
control,
|
||||||
|
name,
|
||||||
|
...props
|
||||||
|
}: DateInputProps<T>) => {
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
render={({ field }) => (
|
||||||
|
<DatePicker
|
||||||
|
locale={"de"}
|
||||||
|
onChange={(date) => field.onChange(date)}
|
||||||
|
selected={field.value}
|
||||||
|
{...(props as any)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,9 +3,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"exports": {
|
"exports": {
|
||||||
"./button": "./src/button.tsx",
|
".": "./src/index.ts"
|
||||||
"./card": "./src/card.tsx",
|
|
||||||
"./code": "./src/code.tsx"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --max-warnings 0",
|
"lint": "eslint . --max-warnings 0",
|
||||||
@@ -14,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/eslint-config": "*",
|
"@repo/eslint-config": "*",
|
||||||
|
"@repo/db": "*",
|
||||||
"@repo/typescript-config": "*",
|
"@repo/typescript-config": "*",
|
||||||
"@turbo/gen": "^1.12.4",
|
"@turbo/gen": "^1.12.4",
|
||||||
"@types/node": "^20.11.24",
|
"@types/node": "^20.11.24",
|
||||||
|
|||||||
4
packages/ui/src/.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
declare module "*.png" {
|
||||||
|
const value: string;
|
||||||
|
export = value;
|
||||||
|
}
|
||||||
28
packages/ui/src/Badge/Badge.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { BADGES } from "@repo/db";
|
||||||
|
import P1 from "./p-1.png";
|
||||||
|
import P2 from "./p-2.png";
|
||||||
|
import P3 from "./p-3.png";
|
||||||
|
import D1 from "./d-1.png";
|
||||||
|
import D2 from "./d-2.png";
|
||||||
|
import D3 from "./d-3.png";
|
||||||
|
import DAY1 from "./day-1-member.png";
|
||||||
|
|
||||||
|
const BadgeImage = {
|
||||||
|
[BADGES.P1]: P1,
|
||||||
|
[BADGES.P2]: P2,
|
||||||
|
[BADGES.P3]: P3,
|
||||||
|
[BADGES.D1]: D1,
|
||||||
|
[BADGES.D2]: D2,
|
||||||
|
[BADGES.D3]: D3,
|
||||||
|
[BADGES.DAY1]: DAY1,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Badge = ({ name }: { name: BADGES }) => {
|
||||||
|
const image = BadgeImage[name];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="badge">
|
||||||
|
<img src={image} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
BIN
packages/ui/src/Badge/d-1.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
packages/ui/src/Badge/d-2.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
packages/ui/src/Badge/d-3.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
packages/ui/src/Badge/day-1-member.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
packages/ui/src/Badge/p-1.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
packages/ui/src/Badge/p-2.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
packages/ui/src/Badge/p-3.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
@@ -1,20 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { ReactNode } from "react";
|
|
||||||
|
|
||||||
interface ButtonProps {
|
|
||||||
children: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
appName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Button = ({ children, className, appName }: ButtonProps) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className={className}
|
|
||||||
onClick={() => alert(`Hello from your ${appName} app!`)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { type JSX } from "react";
|
|
||||||
|
|
||||||
export function Card({
|
|
||||||
className,
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
href,
|
|
||||||
}: {
|
|
||||||
className?: string;
|
|
||||||
title: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
href: string;
|
|
||||||
}): JSX.Element {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
className={className}
|
|
||||||
href={`${href}?utm_source=create-turbo&utm_medium=basic&utm_campaign=create-turbo"`}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
{title} <span>-></span>
|
|
||||||
</h2>
|
|
||||||
<p>{children}</p>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { type JSX } from "react";
|
|
||||||
|
|
||||||
export function Code({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}): JSX.Element {
|
|
||||||
return <code className={className}>{children}</code>;
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import { Event, Participant } from "@repo/db";
|
import { Event, Participant } from "@repo/db";
|
||||||
|
|
||||||
export const participantCompleted = (
|
export const eventCompleted = (event: Event, participant?: Participant) => {
|
||||||
event: Event,
|
if (!participant) return false;
|
||||||
participant: Participant,
|
|
||||||
) => {
|
|
||||||
if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted)
|
if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted)
|
||||||
return false;
|
return false;
|
||||||
if (event.hasPresenceEvents && !participant.attended) return false;
|
if (event.hasPresenceEvents && !participant.attended) return false;
|
||||||
2
packages/ui/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./Badge/Badge";
|
||||||
|
export * from "./helper/event";
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extends": "@repo/typescript-config/react-library.json",
|
"extends": "@repo/typescript-config/react-library.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist"
|
"outDir": "dist",
|
||||||
|
"allowImportingTsExtensions": false
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src", "src/.d.ts"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||