feat:multiple bezoekers
This commit is contained in:
@@ -40,7 +40,7 @@ export function CookieConsent() {
|
||||
<strong>We gebruiken cookies</strong>
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-white/80">
|
||||
We gebruiken analytische cookies om onze website te verbeteren.
|
||||
We gebruiken analytische cookies om onze website te verbeteren.{" "}
|
||||
<a href="/privacy" className="underline hover:text-white">
|
||||
Meer informatie
|
||||
</a>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,11 +9,6 @@ const faqQuestions = [
|
||||
answer:
|
||||
"Iedereen! Of je nu een doorgewinterde artiest bent of voor het eerst op een podium staat — de open mic night is er voor alle niveaus en ervaringen.",
|
||||
},
|
||||
{
|
||||
question: "Is er een minimumleeftijd?",
|
||||
answer:
|
||||
"Als je wilt optreden, moet je minimaal 15 jaar oud zijn & toestemming hebben van je ouders. Er is geen maximumleeftijd — iedereen is welkom!",
|
||||
},
|
||||
{
|
||||
question: "Hoelang mag mijn optreden duren?",
|
||||
answer:
|
||||
|
||||
@@ -37,6 +37,9 @@ export const Route = createFileRoute("/admin")({
|
||||
function AdminPage() {
|
||||
const navigate = useNavigate();
|
||||
const [search, setSearch] = useState("");
|
||||
const [registrationType, setRegistrationType] = useState<
|
||||
"performer" | "watcher" | ""
|
||||
>("");
|
||||
const [artForm, setArtForm] = useState("");
|
||||
const [fromDate, setFromDate] = useState("");
|
||||
const [toDate, setToDate] = useState("");
|
||||
@@ -49,6 +52,7 @@ function AdminPage() {
|
||||
orpc.getRegistrations.queryOptions({
|
||||
input: {
|
||||
search: search || undefined,
|
||||
registrationType: registrationType || undefined,
|
||||
artForm: artForm || undefined,
|
||||
fromDate: fromDate || undefined,
|
||||
toDate: toDate || undefined,
|
||||
@@ -120,6 +124,29 @@ function AdminPage() {
|
||||
const adminRequests = adminRequestsQuery.data ?? [];
|
||||
const pendingRequests = adminRequests.filter((r) => r.status === "pending");
|
||||
|
||||
const performerCount =
|
||||
stats?.byType.find((t) => t.registrationType === "performer")?.count ?? 0;
|
||||
const watcherCount =
|
||||
stats?.byType.find((t) => t.registrationType === "watcher")?.count ?? 0;
|
||||
|
||||
// Calculate total attendees including guests
|
||||
const totalRegistrations = registrationsQuery.data?.data ?? [];
|
||||
const watcherWithGuests = totalRegistrations.filter(
|
||||
(r) => r.registrationType === "watcher" && r.guests,
|
||||
);
|
||||
const totalGuestCount = watcherWithGuests.reduce((sum, r) => {
|
||||
try {
|
||||
const guests = JSON.parse(r.guests as string);
|
||||
return sum + (Array.isArray(guests) ? guests.length : 0);
|
||||
} catch {
|
||||
return sum;
|
||||
}
|
||||
}, 0);
|
||||
const totalWatcherAttendees = watcherCount + totalGuestCount;
|
||||
const totalDrinkCardValue = totalRegistrations
|
||||
.filter((r) => r.registrationType === "watcher")
|
||||
.reduce((sum, r) => sum + (r.drinkCardValue ?? 0), 0);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#214e51]">
|
||||
{/* Header */}
|
||||
@@ -207,7 +234,7 @@ function AdminPage() {
|
||||
)}
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="mb-8 grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div className="mb-8 grid grid-cols-1 gap-6 md:grid-cols-4">
|
||||
<Card className="border-white/10 bg-white/5">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription className="text-white/60">
|
||||
@@ -238,23 +265,61 @@ function AdminPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-white/10 bg-white/5">
|
||||
<Card className="border-amber-400/20 bg-amber-400/5">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription className="text-white/60">
|
||||
Per kunstvorm
|
||||
<CardDescription className="text-amber-300/70">
|
||||
Artiesten
|
||||
</CardDescription>
|
||||
<div className="mt-2 space-y-1">
|
||||
{stats?.byArtForm.slice(0, 5).map((item) => (
|
||||
<CardTitle className="font-['Intro',sans-serif] text-4xl text-amber-300">
|
||||
{performerCount}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-1">
|
||||
{stats?.byArtForm.slice(0, 4).map((item) => (
|
||||
<div
|
||||
key={item.artForm}
|
||||
className="flex items-center justify-between text-sm"
|
||||
className="flex items-center justify-between text-xs"
|
||||
>
|
||||
<span className="text-white/80">{item.artForm}</span>
|
||||
<span className="text-white">{item.count}</span>
|
||||
<span className="text-amber-300/70">
|
||||
{item.artForm || "Onbekend"}
|
||||
</span>
|
||||
<span className="text-amber-300">{item.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-teal-400/20 bg-teal-400/5">
|
||||
<CardHeader className="pb-2">
|
||||
<CardDescription className="text-teal-300/70">
|
||||
Bezoekers
|
||||
</CardDescription>
|
||||
<CardTitle className="font-['Intro',sans-serif] text-4xl text-teal-300">
|
||||
{watcherCount}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-1 text-xs">
|
||||
{totalGuestCount > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-teal-300/70">Inclusief gasten</span>
|
||||
<span className="text-teal-300">+{totalGuestCount}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-teal-300/70">Totaal aanwezig</span>
|
||||
<span className="font-semibold text-teal-300">
|
||||
{totalWatcherAttendees}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-teal-300/70">Drinkkaart</span>
|
||||
<span className="text-teal-300">€{totalDrinkCardValue}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -266,7 +331,7 @@ function AdminPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-5">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="search"
|
||||
@@ -286,6 +351,35 @@ function AdminPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="typeFilter"
|
||||
className="mb-2 block text-sm text-white/60"
|
||||
>
|
||||
Type
|
||||
</label>
|
||||
<select
|
||||
id="typeFilter"
|
||||
value={registrationType}
|
||||
onChange={(e) =>
|
||||
setRegistrationType(
|
||||
e.target.value as "performer" | "watcher" | "",
|
||||
)
|
||||
}
|
||||
className="w-full rounded-md border border-white/20 bg-white/10 px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-white/20"
|
||||
>
|
||||
<option value="" className="bg-[#214e51]">
|
||||
Alle types
|
||||
</option>
|
||||
<option value="performer" className="bg-[#214e51]">
|
||||
Artiesten
|
||||
</option>
|
||||
<option value="watcher" className="bg-[#214e51]">
|
||||
Bezoekers
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="artForm"
|
||||
@@ -369,11 +463,20 @@ function AdminPage() {
|
||||
Telefoon
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left font-medium text-sm text-white/60">
|
||||
Kunstvorm
|
||||
Type
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left font-medium text-sm text-white/60">
|
||||
Kunstvorm / Drinkkaart
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left font-medium text-sm text-white/60">
|
||||
Gezelschap
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left font-medium text-sm text-white/60">
|
||||
Ervaring
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left font-medium text-sm text-white/60">
|
||||
16+
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left font-medium text-sm text-white/60">
|
||||
Datum
|
||||
</th>
|
||||
@@ -383,7 +486,7 @@ function AdminPage() {
|
||||
{registrationsQuery.isLoading ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={6}
|
||||
colSpan={9}
|
||||
className="px-6 py-8 text-center text-white/60"
|
||||
>
|
||||
Laden...
|
||||
@@ -392,36 +495,83 @@ function AdminPage() {
|
||||
) : registrations.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={6}
|
||||
colSpan={9}
|
||||
className="px-6 py-8 text-center text-white/60"
|
||||
>
|
||||
Geen registraties gevonden
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
registrations.map((reg) => (
|
||||
<tr
|
||||
key={reg.id}
|
||||
className="border-white/5 border-b hover:bg-white/5"
|
||||
>
|
||||
<td className="px-6 py-4 text-white">
|
||||
{reg.firstName} {reg.lastName}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-white/80">{reg.email}</td>
|
||||
<td className="px-6 py-4 text-white/80">
|
||||
{reg.phone || "-"}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-white/80">
|
||||
{reg.artForm}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-white/80">
|
||||
{reg.experience || "-"}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-white/60">
|
||||
{new Date(reg.createdAt).toLocaleDateString("nl-BE")}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
registrations.map((reg) => {
|
||||
const isPerformer = reg.registrationType === "performer";
|
||||
return (
|
||||
<tr
|
||||
key={reg.id}
|
||||
className="border-white/5 border-b hover:bg-white/5"
|
||||
>
|
||||
<td className="px-6 py-4 text-white">
|
||||
{reg.firstName} {reg.lastName}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-white/80">
|
||||
{reg.email}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-white/80">
|
||||
{reg.phone || "-"}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2.5 py-0.5 font-semibold text-xs ${isPerformer ? "bg-amber-400/15 text-amber-300" : "bg-teal-400/15 text-teal-300"}`}
|
||||
>
|
||||
{isPerformer ? "Artiest" : "Bezoeker"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-white/80">
|
||||
{isPerformer
|
||||
? reg.artForm || "-"
|
||||
: `€${reg.drinkCardValue ?? 5} drinkkaart`}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-white/80">
|
||||
{isPerformer
|
||||
? "-"
|
||||
: (() => {
|
||||
if (!reg.guests) return "-";
|
||||
try {
|
||||
const guests = JSON.parse(
|
||||
reg.guests as string,
|
||||
);
|
||||
const count = Array.isArray(guests)
|
||||
? guests.length
|
||||
: 0;
|
||||
return count > 0
|
||||
? `${count} gast${count === 1 ? "" : "en"}`
|
||||
: "-";
|
||||
} catch {
|
||||
return "-";
|
||||
}
|
||||
})()}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-white/80">
|
||||
{isPerformer ? reg.experience || "-" : "-"}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-white/80">
|
||||
{isPerformer ? (
|
||||
reg.isOver16 ? (
|
||||
<span className="text-green-400">✓</span>
|
||||
) : (
|
||||
<span className="text-red-400">✗</span>
|
||||
)
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-white/60">
|
||||
{new Date(reg.createdAt).toLocaleDateString(
|
||||
"nl-BE",
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -8,6 +8,31 @@ export const Route = createFileRoute("/manage/$token")({
|
||||
component: ManageRegistrationPage,
|
||||
});
|
||||
|
||||
interface GuestEntry {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
function parseGuests(raw: string | null | undefined): GuestEntry[] {
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.map((g) => ({
|
||||
firstName: g.firstName ?? "",
|
||||
lastName: g.lastName ?? "",
|
||||
email: g.email ?? "",
|
||||
phone: g.phone ?? "",
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function ManageRegistrationPage() {
|
||||
const { token } = useParams({ from: "/manage/$token" });
|
||||
const queryClient = useQueryClient();
|
||||
@@ -18,11 +43,13 @@ function ManageRegistrationPage() {
|
||||
lastName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
wantsToPerform: false,
|
||||
registrationType: "watcher" as "performer" | "watcher",
|
||||
artForm: "",
|
||||
experience: "",
|
||||
isOver16: false,
|
||||
extraQuestions: "",
|
||||
});
|
||||
const [formGuests, setFormGuests] = useState<GuestEntry[]>([]);
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
...orpc.getRegistrationByToken.queryOptions({ input: { token } }),
|
||||
@@ -58,30 +85,41 @@ function ManageRegistrationPage() {
|
||||
lastName: data.lastName,
|
||||
email: data.email,
|
||||
phone: data.phone || "",
|
||||
wantsToPerform: data.wantsToPerform ?? false,
|
||||
registrationType:
|
||||
(data.registrationType as "performer" | "watcher") ?? "watcher",
|
||||
artForm: data.artForm || "",
|
||||
experience: data.experience || "",
|
||||
isOver16: data.isOver16 ?? false,
|
||||
extraQuestions: data.extraQuestions || "",
|
||||
});
|
||||
setFormGuests(parseGuests(data.guests));
|
||||
setIsEditing(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const isPerformer = formData.registrationType === "performer";
|
||||
updateMutation.mutate({
|
||||
token,
|
||||
firstName: formData.firstName.trim(),
|
||||
lastName: formData.lastName.trim(),
|
||||
email: formData.email.trim(),
|
||||
phone: formData.phone.trim() || undefined,
|
||||
wantsToPerform: formData.wantsToPerform,
|
||||
artForm: formData.wantsToPerform
|
||||
? formData.artForm.trim() || undefined
|
||||
: undefined,
|
||||
experience: formData.wantsToPerform
|
||||
registrationType: formData.registrationType,
|
||||
artForm: isPerformer ? formData.artForm.trim() || undefined : undefined,
|
||||
experience: isPerformer
|
||||
? formData.experience.trim() || undefined
|
||||
: undefined,
|
||||
isOver16: isPerformer ? formData.isOver16 : false,
|
||||
guests: isPerformer
|
||||
? []
|
||||
: formGuests.map((g) => ({
|
||||
firstName: g.firstName.trim(),
|
||||
lastName: g.lastName.trim(),
|
||||
email: g.email.trim() || undefined,
|
||||
phone: g.phone.trim() || undefined,
|
||||
})),
|
||||
extraQuestions: formData.extraQuestions.trim() || undefined,
|
||||
});
|
||||
};
|
||||
@@ -96,6 +134,33 @@ function ManageRegistrationPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddGuest = () => {
|
||||
if (formGuests.length >= 9) return;
|
||||
setFormGuests((prev) => [
|
||||
...prev,
|
||||
{ firstName: "", lastName: "", email: "", phone: "" },
|
||||
]);
|
||||
};
|
||||
|
||||
const handleRemoveGuest = (index: number) => {
|
||||
setFormGuests((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleGuestChange = (
|
||||
index: number,
|
||||
field: keyof GuestEntry,
|
||||
value: string,
|
||||
) => {
|
||||
setFormGuests((prev) => {
|
||||
const next = [...prev];
|
||||
next[index] = { ...next[index], [field]: value };
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const inputClasses =
|
||||
"w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none";
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#214e51]">
|
||||
@@ -133,7 +198,13 @@ function ManageRegistrationPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const isPerformer = data.registrationType === "performer";
|
||||
const viewGuests = parseGuests(data.guests);
|
||||
|
||||
if (isEditing) {
|
||||
const isEditingWatcher = formData.registrationType === "watcher";
|
||||
const totalDrinkCard = isEditingWatcher ? 5 + formGuests.length * 2 : 0;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#214e51]">
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
@@ -161,7 +232,7 @@ function ManageRegistrationPage() {
|
||||
onChange={(e) =>
|
||||
setFormData((p) => ({ ...p, firstName: e.target.value }))
|
||||
}
|
||||
className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -176,7 +247,7 @@ function ManageRegistrationPage() {
|
||||
onChange={(e) =>
|
||||
setFormData((p) => ({ ...p, lastName: e.target.value }))
|
||||
}
|
||||
className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,7 +264,7 @@ function ManageRegistrationPage() {
|
||||
onChange={(e) =>
|
||||
setFormData((p) => ({ ...p, email: e.target.value }))
|
||||
}
|
||||
className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -208,46 +279,57 @@ function ManageRegistrationPage() {
|
||||
onChange={(e) =>
|
||||
setFormData((p) => ({ ...p, phone: e.target.value }))
|
||||
}
|
||||
className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label
|
||||
htmlFor="wantsToPerform"
|
||||
className="flex cursor-pointer items-center gap-4"
|
||||
>
|
||||
<div className="relative flex shrink-0">
|
||||
<input
|
||||
id="wantsToPerform"
|
||||
type="checkbox"
|
||||
checked={formData.wantsToPerform}
|
||||
onChange={(e) =>
|
||||
setFormData((p) => ({
|
||||
...p,
|
||||
wantsToPerform: e.target.checked,
|
||||
artForm: e.target.checked ? p.artForm : "",
|
||||
experience: e.target.checked ? p.experience : "",
|
||||
}))
|
||||
}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div className="h-6 w-6 border-2 border-white/50 bg-transparent transition-colors peer-checked:border-white peer-checked:bg-white" />
|
||||
<svg
|
||||
className="pointer-events-none absolute top-0 left-0 h-6 w-6 scale-0 text-[#214e51] transition-transform peer-checked:scale-100"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
aria-label="Geselecteerd"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
{/* Registration type */}
|
||||
<div>
|
||||
<p className="mb-3 text-white">Type inschrijving</p>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex cursor-pointer items-center gap-3">
|
||||
<div className="relative flex shrink-0">
|
||||
<input
|
||||
type="radio"
|
||||
name="registrationType"
|
||||
value="performer"
|
||||
checked={formData.registrationType === "performer"}
|
||||
onChange={() =>
|
||||
setFormData((p) => ({
|
||||
...p,
|
||||
registrationType: "performer",
|
||||
}))
|
||||
}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white/50 bg-transparent transition-colors peer-checked:border-amber-400 peer-checked:bg-amber-400" />
|
||||
</div>
|
||||
<span className="text-white">Optreden</span>
|
||||
</label>
|
||||
<label className="flex cursor-pointer items-center gap-3">
|
||||
<div className="relative flex shrink-0">
|
||||
<input
|
||||
type="radio"
|
||||
name="registrationType"
|
||||
value="watcher"
|
||||
checked={formData.registrationType === "watcher"}
|
||||
onChange={() =>
|
||||
setFormData((p) => ({
|
||||
...p,
|
||||
registrationType: "watcher",
|
||||
}))
|
||||
}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div className="h-5 w-5 rounded-full border-2 border-white/50 bg-transparent transition-colors peer-checked:border-teal-400 peer-checked:bg-teal-400" />
|
||||
</div>
|
||||
<span className="text-white">Kijken</span>
|
||||
</label>
|
||||
</div>
|
||||
<span className="text-white text-xl">Ik wil optreden</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{formData.wantsToPerform && (
|
||||
<div className="space-y-6 border border-white/20 p-6">
|
||||
{formData.registrationType === "performer" && (
|
||||
<div className="space-y-6 border border-amber-400/20 bg-amber-400/5 p-6">
|
||||
<div>
|
||||
<label htmlFor="artForm" className="mb-2 block text-white">
|
||||
Kunstvorm *
|
||||
@@ -255,16 +337,13 @@ function ManageRegistrationPage() {
|
||||
<input
|
||||
id="artForm"
|
||||
type="text"
|
||||
required={formData.wantsToPerform}
|
||||
required={formData.registrationType === "performer"}
|
||||
value={formData.artForm}
|
||||
onChange={(e) =>
|
||||
setFormData((p) => ({
|
||||
...p,
|
||||
artForm: e.target.value,
|
||||
}))
|
||||
setFormData((p) => ({ ...p, artForm: e.target.value }))
|
||||
}
|
||||
list="artFormSuggestions"
|
||||
className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
|
||||
className={inputClasses}
|
||||
/>
|
||||
<datalist id="artFormSuggestions">
|
||||
<option value="Muziek" />
|
||||
@@ -290,7 +369,7 @@ function ManageRegistrationPage() {
|
||||
}))
|
||||
}
|
||||
list="experienceSuggestions"
|
||||
className="w-full border-white/30 border-b bg-transparent py-2 text-lg text-white placeholder:text-white/40 focus:border-white focus:outline-none"
|
||||
className={inputClasses}
|
||||
/>
|
||||
<datalist id="experienceSuggestions">
|
||||
<option value="Beginner" />
|
||||
@@ -298,6 +377,187 @@ function ManageRegistrationPage() {
|
||||
<option value="Professional" />
|
||||
</datalist>
|
||||
</div>
|
||||
<label className="flex cursor-pointer items-center gap-3">
|
||||
<div className="relative flex shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.isOver16}
|
||||
onChange={(e) =>
|
||||
setFormData((p) => ({
|
||||
...p,
|
||||
isOver16: e.target.checked,
|
||||
}))
|
||||
}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div className="h-6 w-6 border-2 border-white/50 bg-transparent transition-colors peer-checked:border-amber-400 peer-checked:bg-amber-400" />
|
||||
<svg
|
||||
className="pointer-events-none absolute top-0 left-0 h-6 w-6 scale-0 text-[#214e51] transition-transform peer-checked:scale-100"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
aria-label="Geselecteerd"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-white">Ik ben 16 jaar of ouder</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Guests section for watchers */}
|
||||
{isEditingWatcher && (
|
||||
<div className="border border-teal-400/20 bg-teal-400/5 p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-teal-300/80 uppercase tracking-wider">
|
||||
Medebezoekers{" "}
|
||||
<span className="text-white/50 normal-case">
|
||||
({formGuests.length}/9)
|
||||
</span>
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-white/60">
|
||||
Drinkkaart totaal: €{totalDrinkCard} voor{" "}
|
||||
{1 + formGuests.length} personen
|
||||
</p>
|
||||
</div>
|
||||
{formGuests.length < 9 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddGuest}
|
||||
className="flex items-center gap-1.5 rounded border border-teal-400/40 bg-teal-400/10 px-3 py-1.5 text-sm text-teal-300 transition-colors hover:bg-teal-400/20"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 4.5v15m7.5-7.5h-15"
|
||||
/>
|
||||
</svg>
|
||||
Toevoegen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{formGuests.length === 0 && (
|
||||
<p className="text-sm text-white/40">
|
||||
Geen medebezoekers toegevoegd.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{formGuests.map((guest, idx) => (
|
||||
<div
|
||||
key={`edit-guest-${
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: stable index
|
||||
idx
|
||||
}`}
|
||||
className="border border-teal-400/20 p-4"
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="font-medium text-sm text-teal-300">
|
||||
Medebezoeker {idx + 1}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveGuest(idx)}
|
||||
className="text-red-400/70 text-sm transition-colors hover:text-red-300"
|
||||
>
|
||||
Verwijderen
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label
|
||||
htmlFor={`edit-guest-${idx}-firstName`}
|
||||
className="mb-1 block text-sm text-white/70"
|
||||
>
|
||||
Voornaam *
|
||||
</label>
|
||||
<input
|
||||
id={`edit-guest-${idx}-firstName`}
|
||||
type="text"
|
||||
required
|
||||
value={guest.firstName}
|
||||
onChange={(e) =>
|
||||
handleGuestChange(
|
||||
idx,
|
||||
"firstName",
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
placeholder="Voornaam"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor={`edit-guest-${idx}-lastName`}
|
||||
className="mb-1 block text-sm text-white/70"
|
||||
>
|
||||
Achternaam *
|
||||
</label>
|
||||
<input
|
||||
id={`edit-guest-${idx}-lastName`}
|
||||
type="text"
|
||||
required
|
||||
value={guest.lastName}
|
||||
onChange={(e) =>
|
||||
handleGuestChange(idx, "lastName", e.target.value)
|
||||
}
|
||||
placeholder="Achternaam"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor={`edit-guest-${idx}-email`}
|
||||
className="mb-1 block text-sm text-white/70"
|
||||
>
|
||||
E-mail
|
||||
</label>
|
||||
<input
|
||||
id={`edit-guest-${idx}-email`}
|
||||
type="email"
|
||||
value={guest.email}
|
||||
onChange={(e) =>
|
||||
handleGuestChange(idx, "email", e.target.value)
|
||||
}
|
||||
placeholder="optioneel@email.be"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor={`edit-guest-${idx}-phone`}
|
||||
className="mb-1 block text-sm text-white/70"
|
||||
>
|
||||
Telefoon
|
||||
</label>
|
||||
<input
|
||||
id={`edit-guest-${idx}-phone`}
|
||||
type="tel"
|
||||
value={guest.phone}
|
||||
onChange={(e) =>
|
||||
handleGuestChange(idx, "phone", e.target.value)
|
||||
}
|
||||
placeholder="06-12345678"
|
||||
className={inputClasses}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -358,6 +618,19 @@ function ManageRegistrationPage() {
|
||||
Open Mic Night — vrijdag 18 april 2026
|
||||
</p>
|
||||
|
||||
{/* Type badge */}
|
||||
<div
|
||||
className={`mb-6 inline-flex items-center gap-2 rounded-full border px-4 py-1.5 font-semibold text-sm ${isPerformer ? "border-amber-400/40 bg-amber-400/10 text-amber-300" : "border-teal-400/40 bg-teal-400/10 text-teal-300"}`}
|
||||
>
|
||||
{isPerformer ? "Artiest" : "Bezoeker"}
|
||||
{!isPerformer && (
|
||||
<span className="ml-1 opacity-80">
|
||||
— Drinkkaart €{data.drinkCardValue ?? 5}
|
||||
{viewGuests.length > 0 && ` (${1 + viewGuests.length} personen)`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 rounded-lg border border-white/20 bg-white/5 p-6 md:p-8">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
@@ -378,17 +651,51 @@ function ManageRegistrationPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-white/10 border-t pt-6">
|
||||
<p className="mb-2 text-sm text-white/50">Rol</p>
|
||||
<p className="text-lg text-white">
|
||||
{data.wantsToPerform
|
||||
? `Optreden${data.artForm ? ` — ${data.artForm}` : ""}`
|
||||
: "Toeschouwer"}
|
||||
</p>
|
||||
{data.wantsToPerform && data.experience && (
|
||||
<p className="mt-1 text-white/60">{data.experience}</p>
|
||||
)}
|
||||
</div>
|
||||
{isPerformer && (
|
||||
<div className="border-white/10 border-t pt-6">
|
||||
<p className="mb-2 text-sm text-white/50">Optreden</p>
|
||||
<p className="text-lg text-white">
|
||||
{data.artForm || "—"}
|
||||
{data.experience && (
|
||||
<span className="ml-2 text-white/60">
|
||||
({data.experience})
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-white/60">
|
||||
{data.isOver16 ? "16+ bevestigd" : "Leeftijd niet bevestigd"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isPerformer && viewGuests.length > 0 && (
|
||||
<div className="border-white/10 border-t pt-6">
|
||||
<p className="mb-3 text-sm text-white/50">
|
||||
Medebezoekers ({viewGuests.length})
|
||||
</p>
|
||||
<div className="flex flex-col gap-3">
|
||||
{viewGuests.map((g, idx) => (
|
||||
<div
|
||||
key={`view-guest-${
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: stable index
|
||||
idx
|
||||
}`}
|
||||
className="rounded border border-teal-400/20 bg-teal-400/5 p-3"
|
||||
>
|
||||
<p className="text-white">
|
||||
{g.firstName} {g.lastName}
|
||||
</p>
|
||||
{g.email && (
|
||||
<p className="text-sm text-white/60">{g.email}</p>
|
||||
)}
|
||||
{g.phone && (
|
||||
<p className="text-sm text-white/60">{g.phone}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.extraQuestions && (
|
||||
<div className="border-white/10 border-t pt-6">
|
||||
|
||||
@@ -12,19 +12,34 @@ import {
|
||||
} from "../email";
|
||||
import { adminProcedure, protectedProcedure, publicProcedure } from "../index";
|
||||
|
||||
const registrationTypeSchema = z.enum(["performer", "watcher"]);
|
||||
|
||||
const guestSchema = z.object({
|
||||
firstName: z.string().min(1),
|
||||
lastName: z.string().min(1),
|
||||
email: z.string().email().optional().or(z.literal("")),
|
||||
phone: z.string().optional(),
|
||||
});
|
||||
|
||||
const submitRegistrationSchema = z.object({
|
||||
firstName: z.string().min(1),
|
||||
lastName: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
phone: z.string().optional(),
|
||||
wantsToPerform: z.boolean().default(false),
|
||||
registrationType: registrationTypeSchema.default("watcher"),
|
||||
// Performer-specific
|
||||
artForm: z.string().optional(),
|
||||
experience: z.string().optional(),
|
||||
isOver16: z.boolean().optional(),
|
||||
// Watcher-specific: drinkCardValue is computed server-side, guests are named
|
||||
guests: z.array(guestSchema).max(9).optional(),
|
||||
// Shared
|
||||
extraQuestions: z.string().optional(),
|
||||
});
|
||||
|
||||
const getRegistrationsSchema = z.object({
|
||||
search: z.string().optional(),
|
||||
registrationType: registrationTypeSchema.optional(),
|
||||
artForm: z.string().optional(),
|
||||
fromDate: z.string().datetime().optional(),
|
||||
toDate: z.string().datetime().optional(),
|
||||
@@ -48,15 +63,22 @@ export const appRouter = {
|
||||
.input(submitRegistrationSchema)
|
||||
.handler(async ({ input }) => {
|
||||
const managementToken = randomUUID();
|
||||
const isPerformer = input.registrationType === "performer";
|
||||
const guests = isPerformer ? [] : (input.guests ?? []);
|
||||
// €5 for primary registrant + €2 per extra guest
|
||||
const drinkCardValue = isPerformer ? 0 : 5 + guests.length * 2;
|
||||
await db.insert(registration).values({
|
||||
id: randomUUID(),
|
||||
firstName: input.firstName,
|
||||
lastName: input.lastName,
|
||||
email: input.email,
|
||||
phone: input.phone || null,
|
||||
wantsToPerform: input.wantsToPerform,
|
||||
artForm: input.artForm || null,
|
||||
experience: input.experience || null,
|
||||
registrationType: input.registrationType,
|
||||
artForm: isPerformer ? input.artForm || null : null,
|
||||
experience: isPerformer ? input.experience || null : null,
|
||||
isOver16: isPerformer ? (input.isOver16 ?? false) : false,
|
||||
drinkCardValue,
|
||||
guests: guests.length > 0 ? JSON.stringify(guests) : null,
|
||||
extraQuestions: input.extraQuestions || null,
|
||||
managementToken,
|
||||
});
|
||||
@@ -65,7 +87,7 @@ export const appRouter = {
|
||||
to: input.email,
|
||||
firstName: input.firstName,
|
||||
managementToken,
|
||||
wantsToPerform: input.wantsToPerform,
|
||||
wantsToPerform: isPerformer,
|
||||
artForm: input.artForm,
|
||||
}).catch((err) =>
|
||||
console.error("Failed to send confirmation email:", err),
|
||||
@@ -98,9 +120,11 @@ export const appRouter = {
|
||||
lastName: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
phone: z.string().optional(),
|
||||
wantsToPerform: z.boolean().default(false),
|
||||
registrationType: registrationTypeSchema.default("watcher"),
|
||||
artForm: z.string().optional(),
|
||||
experience: z.string().optional(),
|
||||
isOver16: z.boolean().optional(),
|
||||
guests: z.array(guestSchema).max(9).optional(),
|
||||
extraQuestions: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
@@ -119,6 +143,9 @@ export const appRouter = {
|
||||
const row = rows[0];
|
||||
if (!row) throw new Error("Inschrijving niet gevonden of al geannuleerd");
|
||||
|
||||
const isPerformer = input.registrationType === "performer";
|
||||
const guests = isPerformer ? [] : (input.guests ?? []);
|
||||
const drinkCardValue = isPerformer ? 0 : 5 + guests.length * 2;
|
||||
await db
|
||||
.update(registration)
|
||||
.set({
|
||||
@@ -126,9 +153,12 @@ export const appRouter = {
|
||||
lastName: input.lastName,
|
||||
email: input.email,
|
||||
phone: input.phone || null,
|
||||
wantsToPerform: input.wantsToPerform,
|
||||
artForm: input.artForm || null,
|
||||
experience: input.experience || null,
|
||||
registrationType: input.registrationType,
|
||||
artForm: isPerformer ? input.artForm || null : null,
|
||||
experience: isPerformer ? input.experience || null : null,
|
||||
isOver16: isPerformer ? (input.isOver16 ?? false) : false,
|
||||
drinkCardValue,
|
||||
guests: guests.length > 0 ? JSON.stringify(guests) : null,
|
||||
extraQuestions: input.extraQuestions || null,
|
||||
})
|
||||
.where(eq(registration.managementToken, input.token));
|
||||
@@ -137,7 +167,7 @@ export const appRouter = {
|
||||
to: input.email,
|
||||
firstName: input.firstName,
|
||||
managementToken: input.token,
|
||||
wantsToPerform: input.wantsToPerform,
|
||||
wantsToPerform: isPerformer,
|
||||
artForm: input.artForm,
|
||||
}).catch((err) => console.error("Failed to send update email:", err));
|
||||
|
||||
@@ -192,6 +222,12 @@ export const appRouter = {
|
||||
);
|
||||
}
|
||||
|
||||
if (input.registrationType) {
|
||||
conditions.push(
|
||||
eq(registration.registrationType, input.registrationType),
|
||||
);
|
||||
}
|
||||
|
||||
if (input.artForm) {
|
||||
conditions.push(eq(registration.artForm, input.artForm));
|
||||
}
|
||||
@@ -235,20 +271,29 @@ export const appRouter = {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const [totalResult, todayResult, artFormResult] = await Promise.all([
|
||||
db.select({ count: count() }).from(registration),
|
||||
db
|
||||
.select({ count: count() })
|
||||
.from(registration)
|
||||
.where(gte(registration.createdAt, today)),
|
||||
db
|
||||
.select({
|
||||
artForm: registration.artForm,
|
||||
count: count(),
|
||||
})
|
||||
.from(registration)
|
||||
.groupBy(registration.artForm),
|
||||
]);
|
||||
const [totalResult, todayResult, artFormResult, typeResult] =
|
||||
await Promise.all([
|
||||
db.select({ count: count() }).from(registration),
|
||||
db
|
||||
.select({ count: count() })
|
||||
.from(registration)
|
||||
.where(gte(registration.createdAt, today)),
|
||||
db
|
||||
.select({
|
||||
artForm: registration.artForm,
|
||||
count: count(),
|
||||
})
|
||||
.from(registration)
|
||||
.where(eq(registration.registrationType, "performer"))
|
||||
.groupBy(registration.artForm),
|
||||
db
|
||||
.select({
|
||||
registrationType: registration.registrationType,
|
||||
count: count(),
|
||||
})
|
||||
.from(registration)
|
||||
.groupBy(registration.registrationType),
|
||||
]);
|
||||
|
||||
return {
|
||||
total: totalResult[0]?.count ?? 0,
|
||||
@@ -257,6 +302,10 @@ export const appRouter = {
|
||||
artForm: r.artForm,
|
||||
count: r.count,
|
||||
})),
|
||||
byType: typeResult.map((r) => ({
|
||||
registrationType: r.registrationType,
|
||||
count: r.count,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -272,24 +321,46 @@ export const appRouter = {
|
||||
"Last Name",
|
||||
"Email",
|
||||
"Phone",
|
||||
"Wants To Perform",
|
||||
"Type",
|
||||
"Art Form",
|
||||
"Experience",
|
||||
"Is Over 16",
|
||||
"Drink Card Value",
|
||||
"Guest Count",
|
||||
"Guests",
|
||||
"Extra Questions",
|
||||
"Created At",
|
||||
];
|
||||
const rows = data.map((r) => [
|
||||
r.id,
|
||||
r.firstName,
|
||||
r.lastName,
|
||||
r.email,
|
||||
r.phone || "",
|
||||
r.wantsToPerform ? "Yes" : "No",
|
||||
r.artForm || "",
|
||||
r.experience || "",
|
||||
r.extraQuestions || "",
|
||||
r.createdAt.toISOString(),
|
||||
]);
|
||||
const rows = data.map((r) => {
|
||||
const guests: Array<{
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}> = r.guests ? JSON.parse(r.guests) : [];
|
||||
const guestSummary = guests
|
||||
.map(
|
||||
(g) =>
|
||||
`${g.firstName} ${g.lastName}${g.email ? ` <${g.email}>` : ""}${g.phone ? ` (${g.phone})` : ""}`,
|
||||
)
|
||||
.join(" | ");
|
||||
return [
|
||||
r.id,
|
||||
r.firstName,
|
||||
r.lastName,
|
||||
r.email,
|
||||
r.phone || "",
|
||||
r.registrationType,
|
||||
r.artForm || "",
|
||||
r.experience || "",
|
||||
r.isOver16 ? "Yes" : "No",
|
||||
String(r.drinkCardValue ?? 0),
|
||||
String(guests.length),
|
||||
guestSummary,
|
||||
r.extraQuestions || "",
|
||||
r.createdAt.toISOString(),
|
||||
];
|
||||
});
|
||||
|
||||
const csvContent = [
|
||||
headers.join(","),
|
||||
@@ -321,16 +392,17 @@ export const appRouter = {
|
||||
.limit(1);
|
||||
|
||||
if (existingRequest.length > 0) {
|
||||
if (existingRequest[0].status === "pending") {
|
||||
const existing = existingRequest[0]!;
|
||||
if (existing.status === "pending") {
|
||||
return { success: false, message: "Je hebt al een aanvraag openstaan" };
|
||||
}
|
||||
if (existingRequest[0].status === "approved") {
|
||||
if (existing.status === "approved") {
|
||||
return {
|
||||
success: false,
|
||||
message: "Je aanvraag is al goedgekeurd, log opnieuw in",
|
||||
};
|
||||
}
|
||||
if (existingRequest[0].status === "rejected") {
|
||||
if (existing.status === "rejected") {
|
||||
// Allow re-requesting if previously rejected
|
||||
await db
|
||||
.update(adminRequest)
|
||||
@@ -387,7 +459,8 @@ export const appRouter = {
|
||||
throw new Error("Aanvraag niet gevonden");
|
||||
}
|
||||
|
||||
if (request[0].status !== "pending") {
|
||||
const req = request[0]!;
|
||||
if (req.status !== "pending") {
|
||||
throw new Error("Deze aanvraag is al behandeld");
|
||||
}
|
||||
|
||||
@@ -405,7 +478,7 @@ export const appRouter = {
|
||||
await db
|
||||
.update(user)
|
||||
.set({ role: "admin" })
|
||||
.where(eq(user.id, request[0].userId));
|
||||
.where(eq(user.id, req.userId));
|
||||
|
||||
return { success: true, message: "Admin toegang goedgekeurd" };
|
||||
}),
|
||||
@@ -423,7 +496,8 @@ export const appRouter = {
|
||||
throw new Error("Aanvraag niet gevonden");
|
||||
}
|
||||
|
||||
if (request[0].status !== "pending") {
|
||||
const req = request[0]!;
|
||||
if (req.status !== "pending") {
|
||||
throw new Error("Deze aanvraag is al behandeld");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import dotenv from "dotenv";
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
dotenv.config({
|
||||
path: "../../apps/web/.env",
|
||||
path: "../env/.env",
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- Migration: Replace wantsToPerform with registrationType + add isOver16 + drinkCardValue
|
||||
-- Migrate existing 'wants_to_perform' data to 'registration_type' before dropping
|
||||
|
||||
ALTER TABLE `registration` ADD `registration_type` text NOT NULL DEFAULT 'watcher';--> statement-breakpoint
|
||||
ALTER TABLE `registration` ADD `is_over_16` integer NOT NULL DEFAULT false;--> statement-breakpoint
|
||||
ALTER TABLE `registration` ADD `drink_card_value` integer DEFAULT 0;--> statement-breakpoint
|
||||
|
||||
-- Backfill registration_type from wants_to_perform
|
||||
UPDATE `registration` SET `registration_type` = 'performer' WHERE `wants_to_perform` = 1;--> statement-breakpoint
|
||||
UPDATE `registration` SET `registration_type` = 'watcher' WHERE `wants_to_perform` = 0;--> statement-breakpoint
|
||||
|
||||
-- Backfill drink_card_value for watchers
|
||||
UPDATE `registration` SET `drink_card_value` = 5 WHERE `wants_to_perform` = 0;--> statement-breakpoint
|
||||
|
||||
-- Create index on registration_type
|
||||
CREATE INDEX `registration_registrationType_idx` ON `registration` (`registration_type`);--> statement-breakpoint
|
||||
|
||||
-- Drop old wants_to_perform column (SQLite requires recreating the table)
|
||||
-- SQLite doesn't support DROP COLUMN directly in older versions, but Turso/libSQL does
|
||||
ALTER TABLE `registration` DROP COLUMN `wants_to_perform`;
|
||||
2
packages/db/src/migrations/0003_add_guests.sql
Normal file
2
packages/db/src/migrations/0003_add_guests.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- Migration: Add guests column (JSON text) to registration table
|
||||
ALTER TABLE `registration` ADD `guests` text;
|
||||
@@ -15,6 +15,13 @@
|
||||
"when": 1772480778632,
|
||||
"tag": "0001_third_stark_industries",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1772520000000,
|
||||
"tag": "0002_registration_type_redesign",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,11 +9,19 @@ export const registration = sqliteTable(
|
||||
lastName: text("last_name").notNull(),
|
||||
email: text("email").notNull(),
|
||||
phone: text("phone"),
|
||||
wantsToPerform: integer("wants_to_perform", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
// registrationType: 'performer' | 'watcher'
|
||||
registrationType: text("registration_type").notNull().default("watcher"),
|
||||
// Performer-specific fields
|
||||
artForm: text("art_form"),
|
||||
experience: text("experience"),
|
||||
isOver16: integer("is_over_16", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
// Watcher-specific fields
|
||||
drinkCardValue: integer("drink_card_value").default(0),
|
||||
// Guests: JSON array of {firstName, lastName, email?, phone?} objects
|
||||
guests: text("guests"),
|
||||
// Shared
|
||||
extraQuestions: text("extra_questions"),
|
||||
managementToken: text("management_token").unique(),
|
||||
cancelledAt: integer("cancelled_at", { mode: "timestamp_ms" }),
|
||||
@@ -23,6 +31,7 @@ export const registration = sqliteTable(
|
||||
},
|
||||
(table) => [
|
||||
index("registration_email_idx").on(table.email),
|
||||
index("registration_registrationType_idx").on(table.registrationType),
|
||||
index("registration_artForm_idx").on(table.artForm),
|
||||
index("registration_createdAt_idx").on(table.createdAt),
|
||||
index("registration_managementToken_idx").on(table.managementToken),
|
||||
|
||||
@@ -29,7 +29,7 @@ export const web = await TanStackStart("web", {
|
||||
SMTP_PASS: getEnvVar("SMTP_PASS"),
|
||||
SMTP_FROM: getEnvVar("SMTP_FROM"),
|
||||
},
|
||||
domains: ["kunstenkamp.be"],
|
||||
domains: ["kunstenkamp.be", "www.kunstenkamp.be"],
|
||||
});
|
||||
|
||||
console.log(`Web -> ${web.url}`);
|
||||
|
||||
Reference in New Issue
Block a user