106 lines
3.9 KiB
TypeScript
Executable File
106 lines
3.9 KiB
TypeScript
Executable File
import { useEffect, useState } from "react";
|
|
import { useParams, useNavigate } from "react-router-dom";
|
|
import { api, isLoggedIn } from "../api/client";
|
|
|
|
interface InviteInfo {
|
|
id: string;
|
|
band_id: string;
|
|
token: string;
|
|
role: string;
|
|
expires_at: string;
|
|
used_at: string | null;
|
|
}
|
|
|
|
export function InvitePage() {
|
|
const { token } = useParams<{ token: string }>();
|
|
const navigate = useNavigate();
|
|
const [invite, setInvite] = useState<InviteInfo | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [accepting, setAccepting] = useState(false);
|
|
const [done, setDone] = useState(false);
|
|
|
|
const loggedIn = isLoggedIn();
|
|
|
|
useEffect(() => {
|
|
if (!token) return;
|
|
api.get<InviteInfo>(`/invites/${token}`)
|
|
.then(setInvite)
|
|
.catch((err) => setError(err instanceof Error ? err.message : "Invalid invite"));
|
|
}, [token]);
|
|
|
|
async function accept() {
|
|
if (!token) return;
|
|
setAccepting(true);
|
|
try {
|
|
await api.post(`/invites/${token}/accept`, {});
|
|
setDone(true);
|
|
setTimeout(() => navigate("/"), 2000);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to accept invite");
|
|
} finally {
|
|
setAccepting(false);
|
|
}
|
|
}
|
|
|
|
function goLogin() {
|
|
navigate(`/login?next=/invite/${token}`);
|
|
}
|
|
|
|
return (
|
|
<div style={{ background: "var(--bg)", minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--text)" }}>
|
|
<div style={{ background: "var(--bg-subtle)", border: "1px solid var(--border)", borderRadius: 12, padding: 40, maxWidth: 420, width: "100%", textAlign: "center" }}>
|
|
<h1 style={{ color: "var(--accent)", fontFamily: "monospace", marginBottom: 8, fontSize: 22 }}>◈ RehearsalHub</h1>
|
|
<p style={{ color: "var(--text-muted)", fontSize: 13, marginBottom: 28 }}>Band invite</p>
|
|
|
|
{error && (
|
|
<div style={{ background: "var(--danger-bg)", border: "1px solid var(--danger)", borderRadius: 6, padding: "12px 16px", color: "var(--danger)", fontSize: 13, marginBottom: 20 }}>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{done && (
|
|
<div style={{ color: "var(--teal)", fontSize: 14 }}>
|
|
Joined! Redirecting…
|
|
</div>
|
|
)}
|
|
|
|
{!done && invite && (
|
|
<>
|
|
<p style={{ color: "var(--text)", fontSize: 15, marginBottom: 6 }}>
|
|
You've been invited to join a band as <strong style={{ color: "var(--accent)" }}>{invite.role}</strong>.
|
|
</p>
|
|
<p style={{ color: "var(--text-muted)", fontSize: 12, marginBottom: 28 }}>
|
|
Expires {new Date(invite.expires_at).toLocaleDateString()}
|
|
{invite.used_at && " · Already used"}
|
|
</p>
|
|
|
|
{loggedIn ? (
|
|
<button
|
|
onClick={accept}
|
|
disabled={accepting || !!invite.used_at}
|
|
style={{ width: "100%", background: "var(--accent)", border: "none", borderRadius: 8, color: "var(--accent-fg)", cursor: "pointer", padding: "12px 0", fontWeight: 700, fontSize: 15 }}
|
|
>
|
|
{accepting ? "Joining…" : "Accept Invite"}
|
|
</button>
|
|
) : (
|
|
<div>
|
|
<p style={{ color: "var(--text-muted)", fontSize: 13, marginBottom: 16 }}>Log in or register to accept this invite.</p>
|
|
<button
|
|
onClick={goLogin}
|
|
style={{ width: "100%", background: "var(--accent)", border: "none", borderRadius: 8, color: "var(--accent-fg)", cursor: "pointer", padding: "12px 0", fontWeight: 700, fontSize: 15 }}
|
|
>
|
|
Log in / Register
|
|
</button>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{!done && !invite && !error && (
|
|
<p style={{ color: "var(--text-muted)" }}>Loading invite…</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|