discord link banner
This commit is contained in:
@@ -1,12 +1,102 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useAuth } from "@/components/AuthProvider";
|
import { useAuth } from "@/components/AuthProvider";
|
||||||
import { useTrips } from "@/lib/useTrips";
|
import { useTrips } from "@/lib/useTrips";
|
||||||
import { c2api } from "@/lib/c2api";
|
import { c2api } from "@/lib/c2api";
|
||||||
import type { TripRecord } from "@/lib/types";
|
import type { TripRecord } from "@/lib/types";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Discord link banner
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function DiscordLinkBanner() {
|
||||||
|
const [status, setStatus] = useState<{ linked: boolean; discord_username?: string } | null>(null);
|
||||||
|
const [code, setCode] = useState<string | null>(null);
|
||||||
|
const [codeExpiry, setCodeExpiry] = useState<number | null>(null); // minutes
|
||||||
|
const [generating, setGenerating] = useState(false);
|
||||||
|
const [unlinking, setUnlinking] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
c2api.getLinkStatus().then(setStatus).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function generateCode() {
|
||||||
|
setGenerating(true);
|
||||||
|
try {
|
||||||
|
const res = await c2api.generateLinkCode();
|
||||||
|
if (res.already_linked) {
|
||||||
|
setStatus((prev) => prev ? { ...prev, linked: true } : prev);
|
||||||
|
} else if (res.code) {
|
||||||
|
setCode(res.code);
|
||||||
|
setCodeExpiry(res.expires_minutes ?? 15);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setGenerating(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unlink() {
|
||||||
|
setUnlinking(true);
|
||||||
|
try {
|
||||||
|
await c2api.unlinkDiscord();
|
||||||
|
setStatus({ linked: false });
|
||||||
|
setCode(null);
|
||||||
|
} finally {
|
||||||
|
setUnlinking(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === null) return null; // still loading
|
||||||
|
|
||||||
|
if (status.linked) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between bg-gray-900 border border-gray-800 rounded-xl px-4 py-3 text-sm">
|
||||||
|
<span className="text-gray-400">
|
||||||
|
Discord linked{status.discord_username ? ` as @${status.discord_username}` : ""}.
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={unlink}
|
||||||
|
disabled={unlinking}
|
||||||
|
className="text-xs text-gray-600 hover:text-gray-400 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{unlinking ? "Unlinking…" : "Unlink"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-900 border border-amber-800/40 rounded-xl px-4 py-3 space-y-2">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-white font-medium">Link your Discord account</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-0.5">
|
||||||
|
Required to access private trips from Discord or the web.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{!code && (
|
||||||
|
<button
|
||||||
|
onClick={generateCode}
|
||||||
|
disabled={generating}
|
||||||
|
className="shrink-0 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white text-xs rounded-lg px-3 py-1.5 transition-colors"
|
||||||
|
>
|
||||||
|
{generating ? "Generating…" : "Get link code"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{code && (
|
||||||
|
<div className="flex items-center gap-3 bg-gray-800 rounded-lg px-3 py-2">
|
||||||
|
<span className="font-mono text-lg tracking-[0.3em] text-white select-all">{code}</span>
|
||||||
|
<span className="text-gray-500 text-xs">—</span>
|
||||||
|
<span className="text-gray-400 text-xs">Run <span className="font-mono text-gray-300">/link {code}</span> in Discord. Expires in {codeExpiry}m.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function fmtDate(iso: string) {
|
function fmtDate(iso: string) {
|
||||||
return new Date(`${iso}T12:00:00`).toLocaleDateString("en-US", {
|
return new Date(`${iso}T12:00:00`).toLocaleDateString("en-US", {
|
||||||
month: "short",
|
month: "short",
|
||||||
@@ -183,6 +273,8 @@ export default function TripsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
|
<DiscordLinkBanner />
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-white text-xl font-bold font-mono">Trips</h1>
|
<h1 className="text-white text-xl font-bold font-mono">Trips</h1>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
|
|||||||
Reference in New Issue
Block a user