mirror of
https://github.com/arthur-pbty/binouz.git
synced 2026-06-21 05:44:51 +02:00
feat: add authentication and user management features
- 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.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user