Laden...
; + } + + if (transactions.length === 0) { + returnGeen transacties gevonden.
; + } + + return ( +{total} transacties
+ +| + Tijdstip + | ++ Gebruiker + | ++ Bedrag + | ++ Type + | ++ Admin + | ++ Opmerking + | ++ Actie + | +
|---|---|---|---|---|---|---|
| + {date} + | +
+ {t.userName} +{t.userEmail} + |
+ + + {isReversal ? "+ " : "− "} + {formatCents(t.amountCents)} + + | ++ {isReversal ? ( + + ↩ Teruggedraaid + + ) : t.reversedBy ? ( + + Teruggedraaid + + ) : ( + + Afschrijving + + )} + | ++ {t.adminName} + | ++ {t.note || "—"} + | ++ {canReverse && ( + + )} + | +
+ Bevestig afschrijving van{" "} + {formatCents(amountCents)}{" "} + voor {userName} +
++ Nieuw saldo: {formatCents(balance - amountCents)} +
+ {deductMutation.isError && ( ++ {(deductMutation.error as Error)?.message ?? + "Fout bij afschrijving"} +
+ )} +Bedrag
++ Onvoldoende saldo. Huidig saldo: {formatCents(balance)} +
+ )} + ++ Huidig saldo: {formatCents(getDrinkkaartQuery.data.balance)} + {" · "} + +
+ )} ++ {(creditMutation.error as Error)?.message ?? "Fout bij opladen"} +
+ )} + +Kon QR-code niet laden
+ ++ {secondsLeft > 0 ? ( + <> + Vervalt over{" "} + {countdownLabel} + > + ) : ( + Verlopen — vernieuwen... + )} +
++ Camera niet beschikbaar. Plak het QR-token hieronder. +
+Of plak token handmatig:
++ Weet je zeker dat je{" "} + + {formatCents(transaction.amountCents)} + {" "} + wil terugdraaien voor{" "} + {transaction.userName}? +
++ Dit voegt {formatCents(transaction.amountCents)} terug toe aan het + saldo. +
+ + {reverseMutation.isError && ( ++ {(reverseMutation.error as Error)?.message ?? + "Fout bij terugdraaien"} +
+ )} + +{userName}
+{userEmail}
+Saldo
++ {formatCents(balance)} +
+Nog geen opladingen.
; + } + + return ( +{t.reason}
+ )} ++ Bedrag moet tussen {formatCents(TOPUP_MIN_CENTS)} en{" "} + {formatCents(TOPUP_MAX_CENTS)} liggen +
+ )} +Nog geen transacties.
; + } + + return ( +{t.note}
+ )} ++ Maak een gratis account aan om je digitale Drinkkaart te + activeren en saldo op te laden vóór het evenement. +
+Drinkkaart inbegrepen
- Je betaald bij registratie{" "}
+ Je betaald bij registratie
€5 (+ €2 per
medebezoeker) dat gaat naar je drinkkaart.
{guests.length > 0 && (
diff --git a/apps/web/src/lib/drinkkaart.ts b/apps/web/src/lib/drinkkaart.ts
new file mode 100644
index 0000000..807db47
--- /dev/null
+++ b/apps/web/src/lib/drinkkaart.ts
@@ -0,0 +1,18 @@
+// Frontend constants and utilities for the Drinkkaart feature.
+
+export const TOPUP_PRESETS_CENTS = [500, 1000, 2000, 5000] as const;
+// = €5, €10, €20, €50
+
+export const DEDUCTION_PRESETS_CENTS = [150, 200, 300, 500] as const;
+// = €1,50 / €2,00 / €3,00 / €5,00 — typical drink prices
+
+export const TOPUP_MIN_CENTS = 100; // €1
+export const TOPUP_MAX_CENTS = 50000; // €500
+
+/** Format a cents integer as a Belgian euro string, e.g. 1250 → "€ 12,50". */
+export function formatCents(cents: number): string {
+ return new Intl.NumberFormat("nl-BE", {
+ style: "currency",
+ currency: "EUR",
+ }).format(cents / 100);
+}
diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts
index 4bf033d..395189e 100644
--- a/apps/web/src/routeTree.gen.ts
+++ b/apps/web/src/routeTree.gen.ts
@@ -12,10 +12,12 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as TermsRouteImport } from './routes/terms'
import { Route as PrivacyRouteImport } from './routes/privacy'
import { Route as LoginRouteImport } from './routes/login'
+import { Route as DrinkkaartRouteImport } from './routes/drinkkaart'
import { Route as ContactRouteImport } from './routes/contact'
-import { Route as AdminRouteImport } from './routes/admin'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as AdminIndexRouteImport } from './routes/admin/index'
import { Route as ManageTokenRouteImport } from './routes/manage.$token'
+import { Route as AdminDrinkkaartRouteImport } from './routes/admin/drinkkaart'
import { Route as ApiWebhookLemonsqueezyRouteImport } from './routes/api/webhook/lemonsqueezy'
import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc/$'
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
@@ -35,26 +37,36 @@ const LoginRoute = LoginRouteImport.update({
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
+const DrinkkaartRoute = DrinkkaartRouteImport.update({
+ id: '/drinkkaart',
+ path: '/drinkkaart',
+ getParentRoute: () => rootRouteImport,
+} as any)
const ContactRoute = ContactRouteImport.update({
id: '/contact',
path: '/contact',
getParentRoute: () => rootRouteImport,
} as any)
-const AdminRoute = AdminRouteImport.update({
- id: '/admin',
- path: '/admin',
- getParentRoute: () => rootRouteImport,
-} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const AdminIndexRoute = AdminIndexRouteImport.update({
+ id: '/admin/',
+ path: '/admin/',
+ getParentRoute: () => rootRouteImport,
+} as any)
const ManageTokenRoute = ManageTokenRouteImport.update({
id: '/manage/$token',
path: '/manage/$token',
getParentRoute: () => rootRouteImport,
} as any)
+const AdminDrinkkaartRoute = AdminDrinkkaartRouteImport.update({
+ id: '/admin/drinkkaart',
+ path: '/admin/drinkkaart',
+ getParentRoute: () => rootRouteImport,
+} as any)
const ApiWebhookLemonsqueezyRoute = ApiWebhookLemonsqueezyRouteImport.update({
id: '/api/webhook/lemonsqueezy',
path: '/api/webhook/lemonsqueezy',
@@ -73,24 +85,28 @@ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
- '/admin': typeof AdminRoute
'/contact': typeof ContactRoute
+ '/drinkkaart': typeof DrinkkaartRoute
'/login': typeof LoginRoute
'/privacy': typeof PrivacyRoute
'/terms': typeof TermsRoute
+ '/admin/drinkkaart': typeof AdminDrinkkaartRoute
'/manage/$token': typeof ManageTokenRoute
+ '/admin/': typeof AdminIndexRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
'/api/webhook/lemonsqueezy': typeof ApiWebhookLemonsqueezyRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
- '/admin': typeof AdminRoute
'/contact': typeof ContactRoute
+ '/drinkkaart': typeof DrinkkaartRoute
'/login': typeof LoginRoute
'/privacy': typeof PrivacyRoute
'/terms': typeof TermsRoute
+ '/admin/drinkkaart': typeof AdminDrinkkaartRoute
'/manage/$token': typeof ManageTokenRoute
+ '/admin': typeof AdminIndexRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
'/api/webhook/lemonsqueezy': typeof ApiWebhookLemonsqueezyRoute
@@ -98,12 +114,14 @@ export interface FileRoutesByTo {
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
- '/admin': typeof AdminRoute
'/contact': typeof ContactRoute
+ '/drinkkaart': typeof DrinkkaartRoute
'/login': typeof LoginRoute
'/privacy': typeof PrivacyRoute
'/terms': typeof TermsRoute
+ '/admin/drinkkaart': typeof AdminDrinkkaartRoute
'/manage/$token': typeof ManageTokenRoute
+ '/admin/': typeof AdminIndexRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/rpc/$': typeof ApiRpcSplatRoute
'/api/webhook/lemonsqueezy': typeof ApiWebhookLemonsqueezyRoute
@@ -112,36 +130,42 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
- | '/admin'
| '/contact'
+ | '/drinkkaart'
| '/login'
| '/privacy'
| '/terms'
+ | '/admin/drinkkaart'
| '/manage/$token'
+ | '/admin/'
| '/api/auth/$'
| '/api/rpc/$'
| '/api/webhook/lemonsqueezy'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
- | '/admin'
| '/contact'
+ | '/drinkkaart'
| '/login'
| '/privacy'
| '/terms'
+ | '/admin/drinkkaart'
| '/manage/$token'
+ | '/admin'
| '/api/auth/$'
| '/api/rpc/$'
| '/api/webhook/lemonsqueezy'
id:
| '__root__'
| '/'
- | '/admin'
| '/contact'
+ | '/drinkkaart'
| '/login'
| '/privacy'
| '/terms'
+ | '/admin/drinkkaart'
| '/manage/$token'
+ | '/admin/'
| '/api/auth/$'
| '/api/rpc/$'
| '/api/webhook/lemonsqueezy'
@@ -149,12 +173,14 @@ export interface FileRouteTypes {
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
- AdminRoute: typeof AdminRoute
ContactRoute: typeof ContactRoute
+ DrinkkaartRoute: typeof DrinkkaartRoute
LoginRoute: typeof LoginRoute
PrivacyRoute: typeof PrivacyRoute
TermsRoute: typeof TermsRoute
+ AdminDrinkkaartRoute: typeof AdminDrinkkaartRoute
ManageTokenRoute: typeof ManageTokenRoute
+ AdminIndexRoute: typeof AdminIndexRoute
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
ApiRpcSplatRoute: typeof ApiRpcSplatRoute
ApiWebhookLemonsqueezyRoute: typeof ApiWebhookLemonsqueezyRoute
@@ -183,6 +209,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
+ '/drinkkaart': {
+ id: '/drinkkaart'
+ path: '/drinkkaart'
+ fullPath: '/drinkkaart'
+ preLoaderRoute: typeof DrinkkaartRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/contact': {
id: '/contact'
path: '/contact'
@@ -190,13 +223,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ContactRouteImport
parentRoute: typeof rootRouteImport
}
- '/admin': {
- id: '/admin'
- path: '/admin'
- fullPath: '/admin'
- preLoaderRoute: typeof AdminRouteImport
- parentRoute: typeof rootRouteImport
- }
'/': {
id: '/'
path: '/'
@@ -204,6 +230,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/admin/': {
+ id: '/admin/'
+ path: '/admin'
+ fullPath: '/admin/'
+ preLoaderRoute: typeof AdminIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/manage/$token': {
id: '/manage/$token'
path: '/manage/$token'
@@ -211,6 +244,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ManageTokenRouteImport
parentRoute: typeof rootRouteImport
}
+ '/admin/drinkkaart': {
+ id: '/admin/drinkkaart'
+ path: '/admin/drinkkaart'
+ fullPath: '/admin/drinkkaart'
+ preLoaderRoute: typeof AdminDrinkkaartRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/api/webhook/lemonsqueezy': {
id: '/api/webhook/lemonsqueezy'
path: '/api/webhook/lemonsqueezy'
@@ -237,12 +277,14 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
- AdminRoute: AdminRoute,
ContactRoute: ContactRoute,
+ DrinkkaartRoute: DrinkkaartRoute,
LoginRoute: LoginRoute,
PrivacyRoute: PrivacyRoute,
TermsRoute: TermsRoute,
+ AdminDrinkkaartRoute: AdminDrinkkaartRoute,
ManageTokenRoute: ManageTokenRoute,
+ AdminIndexRoute: AdminIndexRoute,
ApiAuthSplatRoute: ApiAuthSplatRoute,
ApiRpcSplatRoute: ApiRpcSplatRoute,
ApiWebhookLemonsqueezyRoute: ApiWebhookLemonsqueezyRoute,
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx
index 0a52b86..04f7c6a 100644
--- a/apps/web/src/routes/__root.tsx
+++ b/apps/web/src/routes/__root.tsx
@@ -9,6 +9,7 @@ import {
} from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { CookieConsent } from "@/components/CookieConsent";
+import { SiteHeader } from "@/components/SiteHeader";
import { Toaster } from "@/components/ui/sonner";
import type { orpc } from "@/utils/orpc";
@@ -165,6 +166,7 @@ function RootDocument() {
>
Ga naar hoofdinhoud
+
+ Scan een QR-code of laad handmatig op. +
+QR-code wordt gecontroleerd...
++ Betaling verwerkt +
++ {scanState.userName} — nieuw saldo:{" "} + + {formatCents(scanState.balanceAfter)} + +
+Fout
++ {scanState.message} +
+Huidig saldo
++ {data ? formatCents(data.balance) : "—"} +
+ +Laden...
+ )} +Laden...
+ )} +| + + | +