feat:drinkkaart

This commit is contained in:
2026-03-03 23:52:49 +01:00
parent b8cefd373d
commit 79869a9a21
35 changed files with 3189 additions and 76 deletions

View File

@@ -0,0 +1,144 @@
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
formatCents,
TOPUP_MAX_CENTS,
TOPUP_MIN_CENTS,
TOPUP_PRESETS_CENTS,
} from "@/lib/drinkkaart";
import { orpc } from "@/utils/orpc";
interface TopUpModalProps {
onClose: () => void;
}
export function TopUpModal({ onClose }: TopUpModalProps) {
const [selectedCents, setSelectedCents] = useState<number>(1000);
const [customEuros, setCustomEuros] = useState("");
const [useCustom, setUseCustom] = useState(false);
const checkoutMutation = useMutation({
...orpc.drinkkaart.getTopUpCheckoutUrl.mutationOptions(),
onSuccess: (data: { checkoutUrl: string }) => {
window.location.href = data.checkoutUrl;
},
onError: (err: Error) => {
console.error("Checkout error:", err);
},
});
const amountCents = useCustom
? Math.round((Number.parseFloat(customEuros || "0") || 0) * 100)
: selectedCents;
const isValidAmount =
Number.isInteger(amountCents) &&
amountCents >= TOPUP_MIN_CENTS &&
amountCents <= TOPUP_MAX_CENTS;
const handleSubmit = () => {
if (!isValidAmount) return;
checkoutMutation.mutate({ amountCents });
};
return (
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
<button
type="button"
className="absolute inset-0 bg-black/60"
aria-label="Sluiten"
onClick={onClose}
/>
<div className="relative z-10 w-full max-w-sm rounded-t-2xl bg-[#1a3d40] p-6 sm:rounded-2xl">
<div className="mb-5 flex items-center justify-between">
<h2 className="font-['Intro',sans-serif] text-white text-xl">
Drinkkaart opladen
</h2>
<button
type="button"
onClick={onClose}
className="text-white/50 hover:text-white"
aria-label="Sluiten"
>
</button>
</div>
{/* Preset amounts */}
<div className="mb-4 grid grid-cols-4 gap-2">
{TOPUP_PRESETS_CENTS.map((cents) => (
<button
key={cents}
type="button"
onClick={() => {
setSelectedCents(cents);
setUseCustom(false);
}}
className={`rounded-lg border py-3 font-medium text-sm transition-colors ${
!useCustom && selectedCents === cents
? "border-white bg-white text-[#214e51]"
: "border-white/20 text-white hover:border-white/50"
}`}
>
{formatCents(cents)}
</button>
))}
</div>
{/* Custom amount */}
<div className="mb-5">
<label
htmlFor="custom-amount"
className="mb-2 block text-sm text-white/60"
>
Eigen bedrag ( 1 500)
</label>
<div className="relative">
<span className="absolute top-1/2 left-3 -translate-y-1/2 text-white/50">
</span>
<Input
id="custom-amount"
type="number"
min="1"
max="500"
step="0.50"
placeholder="Bijv. 15"
value={customEuros}
onChange={(e) => {
setCustomEuros(e.target.value);
setUseCustom(true);
}}
className="border-white/20 bg-white/10 pl-8 text-white placeholder:text-white/30"
/>
</div>
{useCustom && !isValidAmount && customEuros !== "" && (
<p className="mt-1 text-red-400 text-sm">
Bedrag moet tussen {formatCents(TOPUP_MIN_CENTS)} en{" "}
{formatCents(TOPUP_MAX_CENTS)} liggen
</p>
)}
</div>
<div className="mb-3 rounded-lg bg-white/5 px-4 py-3">
<span className="text-sm text-white/60">Te betalen: </span>
<span className="font-semibold text-white">
{isValidAmount ? formatCents(amountCents) : "—"}
</span>
</div>
<Button
onClick={handleSubmit}
disabled={!isValidAmount || checkoutMutation.isPending}
className="w-full bg-white font-semibold text-[#214e51] hover:bg-white/90 disabled:opacity-50"
>
{checkoutMutation.isPending
? "Doorsturen naar betaling..."
: "Naar betaling"}
</Button>
</div>
</div>
);
}