luowuyin's picture
25:05:05 10:41:39 v0.3.7
a572854
"use client";
import { useState } from "react";
import { toast, Toaster } from "sonner";
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { HelpCircle, Loader2, Key, LockKeyhole } from "lucide-react";
import { cn } from "@/lib/utils";
import { AnimatedGridPattern } from "@/components/ui/animated-grid-pattern";
export default function TokenPage() {
const [token, setToken] = useState("");
const [loading, setLoading] = useState(false);
const [showToken, setShowToken] = useState(false);
const { t } = useTranslation("common");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!token.trim()) {
toast.error(t("auth.accessTokenRequired"));
return;
}
setLoading(true);
try {
localStorage.setItem("access_token", token);
const res = await fetch("/api/v1/config", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (res.ok) {
toast.success(t("auth.loginSuccess"));
window.location.href = "/";
} else {
toast.error(t("auth.invalidToken"));
localStorage.removeItem("access_token");
}
} catch (error) {
toast.error(t("auth.verificationFailed"));
localStorage.removeItem("access_token");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-rose-50 via-slate-50 to-teal-50 relative overflow-hidden">
<Toaster
richColors
position="top-center"
theme="light"
expand
duration={1500}
/>
<AnimatedGridPattern
numSquares={30}
maxOpacity={0.03}
duration={3}
repeatDelay={1}
className={cn(
"[mask-image:radial-gradient(1200px_circle_at_center,white,transparent)]",
"absolute inset-x-0 inset-y-[-30%] h-[160%] w-full skew-y-12 z-0"
)}
/>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[1000px] h-[1000px] bg-gradient-to-br from-rose-100/20 via-slate-100/20 to-teal-100/20 rounded-full blur-3xl opacity-40" />
<div className="absolute top-1/4 left-1/4 w-[500px] h-[500px] bg-gradient-to-br from-pink-100/10 to-indigo-100/10 rounded-full blur-3xl opacity-30" />
<div className="absolute bottom-1/4 right-1/4 w-[700px] h-[700px] bg-gradient-to-br from-teal-100/10 to-slate-100/10 rounded-full blur-3xl opacity-30" />
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="w-full max-w-md space-y-10 z-10 px-6"
>
<motion.div
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="text-center space-y-6"
>
<div className="mx-auto w-20 h-20 flex items-center justify-center">
<img
src="/icon.png"
alt="Logo"
className="w-20 h-20 object-contain drop-shadow-xl"
/>
</div>
<h1 className="text-3xl font-bold bg-gradient-to-br from-slate-800 via-slate-700 to-slate-800 bg-clip-text text-transparent">
{t("common.appName")}
</h1>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
className="backdrop-blur-[20px] bg-white/[0.08] p-10 rounded-[2.5rem] border border-white/20 shadow-2xl relative overflow-hidden
hover:shadow-[0_8px_60px_rgba(120,119,198,0.15)] transition-shadow duration-300 group"
>
<div
className="absolute inset-0 rounded-[2.5rem] p-[2px]
bg-gradient-to-br from-white/30 via-transparent to-transparent
[mask:linear-gradient(black,black)_content-box,linear-gradient(black,black)]
[mask-composite:xor] opacity-30 group-hover:opacity-50 transition-opacity"
></div>
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-slate-100/5 via-transparent to-transparent opacity-20" />
<motion.form
onSubmit={handleSubmit}
className="space-y-8 relative"
initial="hidden"
animate="visible"
variants={{
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.3,
},
},
}}
>
<motion.div
className="space-y-6"
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}}
>
<div className="flex flex-col gap-4">
<div className="flex items-center gap-3">
<Key className="h-5 w-5 text-slate-500/80" />
<Label
htmlFor="token"
className="text-sm font-medium text-slate-600/90 tracking-wide"
>
{t("auth.accessToken")}
</Label>
<TooltipProvider delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="h-4 w-4 text-slate-400 hover:text-slate-600 cursor-pointer transition-colors" />
</TooltipTrigger>
<TooltipContent
side="right"
className="max-w-[260px] text-xs"
>
{t("auth.accessTokenHelp")}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="relative">
<Input
id="token"
type={showToken ? "text" : "password"}
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder={t("auth.accessTokenPlaceholder")}
className="w-full bg-white/20 border-2 border-slate-300/30 hover:border-slate-400/50
focus:border-slate-500/80 focus:ring-2 focus:ring-slate-400/20
transition-all duration-300 placeholder:text-slate-500/70
shadow-sm hover:shadow-md focus:shadow-lg pl-12
[&:focus]:bg-white/30 backdrop-blur-sm rounded-xl h-14 text-base"
autoComplete="off"
/>
<LockKeyhole className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-500/70" />
</div>
</div>
</motion.div>
<motion.div
className="flex items-center space-x-2"
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}}
>
<Checkbox
id="showToken"
checked={showToken}
onCheckedChange={(checked) => setShowToken(checked as boolean)}
className="text-slate-600 focus:ring-slate-500 border-2 border-slate-300/50 data-[state=checked]:border-slate-600 data-[state=checked]:bg-slate-600/90"
/>
<Label
htmlFor="showToken"
className="text-sm font-medium text-slate-600 cursor-pointer select-none"
>
{t("auth.showToken")}
</Label>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.5 }}
>
<Button
onClick={handleSubmit}
className="w-full h-14 font-semibold bg-gradient-to-r from-slate-700/90 to-slate-800/90
hover:from-slate-800 hover:to-slate-900 text-white/95
shadow-xl shadow-slate-500/10 hover:shadow-slate-500/20
transition-all duration-300 rounded-xl
hover:scale-[0.98] transform-gpu
border border-white/10
hover:after:opacity-100
relative overflow-hidden
after:absolute after:inset-0 after:bg-[radial-gradient(200px_circle_at_center,_rgba(255,255,255,0.15)_0%,_transparent_80%)]
after:opacity-0 after:transition-opacity after:duration-300"
disabled={loading}
size="lg"
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="h-5 w-5 animate-spin" />
{t("auth.verifying")}
</span>
) : (
<span className="tracking-wide">{t("common.confirm")}</span>
)}
</Button>
</motion.div>
</motion.form>
</motion.div>
</motion.div>
</div>
);
}