mirror of
https://github.com/arthur-pbty/binouz.git
synced 2026-06-03 15:07:17 +02:00
b7010a1704
- Implemented AuthButton component for Discord sign-in and sign-out functionality. - Created CopyButton component for copying server IP addresses. - Developed EventCard and GradeCard components for displaying events and grades. - Added Footer and Navbar components for site navigation and information. - Introduced PurchaseButton for handling grade purchases with Stripe integration. - Created SectionHeader component for consistent section titles. - Implemented session management with SessionProvider for NextAuth. - Set up PostgreSQL database with Docker and Prisma for data management. - Added admin guard functionality to restrict access to certain routes. - Configured NextAuth with Discord provider for user authentication. - Defined Prisma schema for user, admin, grade, event, and purchase models. - Seeded database with initial grades and events data. - Added SVG hero image for the landing page. - Extended NextAuth types to include additional user properties.
129 lines
4.0 KiB
TypeScript
129 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
|
|
type PurchaseButtonProps = {
|
|
gradeId: string;
|
|
};
|
|
|
|
type PurchaseState = "idle" | "loading" | "error";
|
|
|
|
const minecraftNameRegex = /^[A-Za-z0-9_]{3,16}$/;
|
|
|
|
export default function PurchaseButton({ gradeId }: PurchaseButtonProps) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [username, setUsername] = useState("");
|
|
const [state, setState] = useState<PurchaseState>("idle");
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const openModal = () => {
|
|
setIsOpen(true);
|
|
setError(null);
|
|
};
|
|
|
|
const closeModal = () => {
|
|
setIsOpen(false);
|
|
setState("idle");
|
|
setError(null);
|
|
};
|
|
|
|
const handleCheckout = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
event.preventDefault();
|
|
const trimmed = username.trim();
|
|
|
|
if (!minecraftNameRegex.test(trimmed)) {
|
|
setError("Enter a valid Minecraft username (3-16, letters, numbers, _).");
|
|
return;
|
|
}
|
|
|
|
setState("loading");
|
|
setError(null);
|
|
|
|
try {
|
|
const res = await fetch("/api/checkout", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ gradeId, minecraftUsername: trimmed }),
|
|
});
|
|
|
|
const data = (await res.json().catch(() => ({}))) as {
|
|
url?: string;
|
|
error?: string;
|
|
};
|
|
|
|
if (!res.ok || !data.url) {
|
|
setState("error");
|
|
setError(data.error ?? "Checkout failed.");
|
|
return;
|
|
}
|
|
|
|
window.location.href = data.url;
|
|
} catch {
|
|
setState("error");
|
|
setError("Checkout failed.");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={openModal}
|
|
className="inline-flex items-center justify-center gap-2 rounded-full bg-linear-to-r from-violet-600 via-indigo-500 to-cyan-400 px-4 py-2 text-xs font-semibold uppercase tracking-[0.2em] text-slate-950 transition hover:brightness-110"
|
|
aria-label="Open checkout"
|
|
>
|
|
<span>Acheter</span>
|
|
</button>
|
|
|
|
{isOpen ? (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/80 px-6"
|
|
onClick={closeModal}
|
|
>
|
|
<div
|
|
className="w-full max-w-md rounded-3xl border border-white/10 bg-slate-950/90 p-6 text-white shadow-[0_30px_80px_rgba(2,6,23,0.6)]"
|
|
onClick={(event) => event.stopPropagation()}
|
|
>
|
|
<p className="text-xs uppercase tracking-[0.4em] text-cyan-200/80">
|
|
Checkout
|
|
</p>
|
|
<h3 className="mt-3 text-2xl font-semibold">
|
|
Enter Minecraft username
|
|
</h3>
|
|
<p className="mt-2 text-sm text-slate-300">
|
|
No account needed. We will send you to Stripe.
|
|
</p>
|
|
<form onSubmit={handleCheckout} className="mt-5 space-y-4">
|
|
<input
|
|
value={username}
|
|
onChange={(event) => setUsername(event.target.value)}
|
|
placeholder="Minecraft username"
|
|
className="w-full rounded-2xl border border-white/10 bg-slate-950/60 px-4 py-3 text-sm text-white"
|
|
/>
|
|
{error ? (
|
|
<p className="text-xs text-rose-300">{error}</p>
|
|
) : null}
|
|
<div className="flex flex-wrap gap-3">
|
|
<button
|
|
type="submit"
|
|
className="flex-1 rounded-2xl bg-linear-to-r from-violet-600 via-indigo-500 to-cyan-400 px-4 py-3 text-xs font-semibold uppercase tracking-[0.2em] text-slate-950"
|
|
disabled={state === "loading"}
|
|
>
|
|
{state === "loading" ? "Redirecting" : "Go to Stripe"}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={closeModal}
|
|
className="rounded-2xl border border-white/10 px-4 py-3 text-xs font-semibold uppercase tracking-[0.2em] text-white/80"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|