| import { useState } from "react"; |
| import { useQuery, useQueryClient } from "@tanstack/react-query"; |
| import { Shield, CheckCircle, ChevronDown, ChevronUp, Loader2 } from "lucide-react"; |
| import { Button } from "@/components/ui/button"; |
| import { Textarea } from "@/components/ui/textarea"; |
| import { useAuth } from "@/contexts/AuthContext"; |
| import { useLang } from "@/contexts/LanguageContext"; |
| import { useToast } from "@/hooks/use-toast"; |
|
|
| const BASE = import.meta.env.BASE_URL.replace(/\/$/, ""); |
| const API_BASE = `${BASE}/api`; |
| const BASE_URL = import.meta.env.BASE_URL.replace(/\/$/, ""); |
|
|
| async function fetchSetupStatus(): Promise<{ refreshTokenConfigured: boolean }> { |
| const r = await fetch(`${API_BASE}/admin/setup-status`, { credentials: "include" }); |
| if (!r.ok) return { refreshTokenConfigured: true }; |
| return r.json(); |
| } |
|
|
| export function SetupWizard() { |
| const { isAdmin, isLoaded } = useAuth(); |
| const { t } = useLang(); |
| const { toast } = useToast(); |
| const qc = useQueryClient(); |
| const [token, setToken] = useState(""); |
| const [saving, setSaving] = useState(false); |
| const [stepsOpen, setStepsOpen] = useState(true); |
|
|
| const { data, isLoading } = useQuery({ |
| queryKey: ["setupStatus"], |
| queryFn: fetchSetupStatus, |
| enabled: isLoaded && isAdmin, |
| staleTime: 0, |
| }); |
|
|
| if (!isLoaded || !isAdmin || isLoading || data?.refreshTokenConfigured) return null; |
|
|
| const handleSave = async () => { |
| if (!token.trim()) return; |
| setSaving(true); |
| try { |
| const r = await fetch(`${API_BASE}/admin/setup`, { |
| method: "POST", |
| credentials: "include", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ refreshToken: token.trim() }), |
| }); |
| const data = await r.json(); |
| if (!r.ok) throw new Error(data.error || "Failed"); |
| await qc.invalidateQueries({ queryKey: ["setupStatus"] }); |
| toast({ description: t.setupSuccess }); |
| } catch (e: any) { |
| toast({ variant: "destructive", description: e.message || t.setupError }); |
| } |
| setSaving(false); |
| }; |
|
|
| return ( |
| <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm"> |
| <div className="w-full max-w-xl bg-card border border-border/80 rounded-2xl shadow-2xl overflow-hidden"> |
| <div className="bg-gradient-to-r from-primary/20 to-accent/10 border-b border-border/50 px-6 py-5 flex items-center gap-3"> |
| <div className="w-10 h-10 rounded-xl bg-primary/20 border border-primary/30 flex items-center justify-center"> |
| <Shield className="w-5 h-5 text-primary" /> |
| </div> |
| <div> |
| <h2 className="text-lg font-bold text-foreground">{t.setupTitle}</h2> |
| <p className="text-xs text-muted-foreground">{t.setupSubtitle}</p> |
| </div> |
| </div> |
| |
| <div className="px-6 py-5 space-y-5 max-h-[75vh] overflow-y-auto"> |
| <div className="rounded-xl border border-border/50 overflow-hidden"> |
| <button |
| onClick={() => setStepsOpen(!stepsOpen)} |
| className="w-full flex items-center justify-between px-4 py-3 text-sm font-medium text-foreground hover:bg-secondary/50 transition-colors" |
| > |
| <span>📖 {t.adminGuideTitle}</span> |
| {stepsOpen ? <ChevronUp className="w-4 h-4 text-muted-foreground" /> : <ChevronDown className="w-4 h-4 text-muted-foreground" />} |
| </button> |
| {stepsOpen && ( |
| <div className="border-t border-border/40 px-4 py-4 space-y-4 bg-black/10"> |
| <ol className="space-y-2.5"> |
| {(t.adminGuideSteps as string[]).map((step, i) => ( |
| <li key={i} className="flex gap-3 text-sm text-muted-foreground"> |
| <span className="flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 border border-primary/30 text-primary text-xs flex items-center justify-center font-bold mt-0.5"> |
| {i + 1} |
| </span> |
| <span dangerouslySetInnerHTML={{ __html: step }} /> |
| </li> |
| ))} |
| </ol> |
| <div className="rounded-lg overflow-hidden border border-border/40"> |
| <img |
| src={`${BASE_URL}/token-guide.png`} |
| alt="Token guide screenshot" |
| className="w-full h-auto" |
| /> |
| </div> |
| </div> |
| )} |
| </div> |
| |
| <div className="space-y-2"> |
| <label className="text-sm font-medium text-foreground">{t.setupPasteLabel}</label> |
| <Textarea |
| value={token} |
| onChange={(e) => setToken(e.target.value)} |
| placeholder={t.setupPastePlaceholder} |
| className="font-mono text-xs bg-background/50 border-border/60 resize-none h-24" |
| spellCheck={false} |
| /> |
| <p className="text-xs text-muted-foreground">{t.setupPasteHint}</p> |
| </div> |
| </div> |
| |
| <div className="border-t border-border/50 px-6 py-4 flex justify-end gap-3 bg-black/10"> |
| <Button |
| onClick={handleSave} |
| disabled={!token.trim() || saving} |
| className="gap-2 min-w-[120px]" |
| > |
| {saving ? ( |
| <><Loader2 className="w-4 h-4 animate-spin" />{t.setupSaving}</> |
| ) : ( |
| <><CheckCircle className="w-4 h-4" />{t.setupConfirm}</> |
| )} |
| </Button> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|