spb/vquant Public MIT
VibeQuant — AI-powered institutional-grade financial intelligence platform.
TypeScript 84.3%
Python 11.7%
JavaScript 1.6%
CSS 1.5%
HTML 0.7%
1/*2 * =============================================================================3 * VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 * File: client/src/pages/auth.tsx6 *7 * Author: Simon-Pierre Boucher8 * Contact: contact@spboucher.ai9 * Website: https://www.spboucher.ai10 * Demo: https://www.vquant.ai11 * License: MIT (see LICENSE)12 *13 * Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import { useState } from "react";18import { User, KeyRound, Sparkles, Copy, Check, ArrowLeft } from "lucide-react";19import { Button } from "@/components/ui/button";20import { Input } from "@/components/ui/input";21import { Label } from "@/components/ui/label";22import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";23import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";24import { Alert, AlertDescription } from "@/components/ui/alert";25import { useLocation } from "wouter";2627export default function Auth() {28 const [, setLocation] = useLocation();29 const [isLoading, setIsLoading] = useState(false);30 const [error, setError] = useState("");31 const [success, setSuccess] = useState("");3233 // Create account state34 const [name, setName] = useState("");35 const [generatedToken, setGeneratedToken] = useState("");36 const [copied, setCopied] = useState(false);3738 // Login state39 const [loginToken, setLoginToken] = useState("");4041 const handleCreate = async (e: React.FormEvent) => {42 e.preventDefault();43 setError("");44 setSuccess("");45 setGeneratedToken("");46 setIsLoading(true);4748 try {49 const response = await fetch("/api/auth/register", {50 method: "POST",51 headers: { "Content-Type": "application/json" },52 body: JSON.stringify({ name: name.trim() }),53 });5455 const data = await response.json();5657 if (!response.ok) {58 throw new Error(data.error || "Erreur lors de la création du compte");59 }6061 setGeneratedToken(data.token);62 setSuccess(`Compte créé avec succès! Bienvenue, ${data.user.displayName}!`);63 } catch (err: any) {64 setError(err.message || "Erreur lors de la création du compte");65 } finally {66 setIsLoading(false);67 }68 };6970 const handleLogin = async (e: React.FormEvent) => {71 e.preventDefault();72 setError("");73 setSuccess("");74 setIsLoading(true);7576 try {77 const response = await fetch("/api/auth/login", {78 method: "POST",79 headers: { "Content-Type": "application/json" },80 body: JSON.stringify({ token: loginToken.trim() }),81 });8283 const data = await response.json();8485 if (!response.ok) {86 throw new Error(data.error || "Token invalide");87 }8889 setSuccess(`Bienvenue, ${data.user.displayName}!`);9091 setTimeout(() => {92 setLocation('/');93 }, 1000);94 } catch (err: any) {95 setError(err.message || "Erreur de connexion");96 } finally {97 setIsLoading(false);98 }99 };100101 const handleCopyToken = async () => {102 try {103 await navigator.clipboard.writeText(generatedToken);104 setCopied(true);105 setTimeout(() => setCopied(false), 2000);106 } catch {107 // Fallback for mobile108 const textarea = document.createElement('textarea');109 textarea.value = generatedToken;110 document.body.appendChild(textarea);111 textarea.select();112 document.execCommand('copy');113 document.body.removeChild(textarea);114 setCopied(true);115 setTimeout(() => setCopied(false), 2000);116 }117 };118119 const handleGoHome = () => {120 setLocation('/');121 };122123 return (124 <div className="min-h-screen bg-background flex flex-col relative overflow-hidden">125 {/* Subtle Background */}126 <div className="fixed inset-0 z-0 pointer-events-none">127 <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,hsl(var(--primary)/0.03),transparent_70%)]" />128 </div>129130 {/* Header */}131 <header className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur-sm">132 <div className="container flex h-14 items-center justify-between px-4">133 <button134 onClick={() => setLocation('/')}135 className="flex items-center gap-2.5 cursor-pointer hover:opacity-80 transition-opacity"136 >137 <Sparkles className="w-5 h-5 text-primary" />138 <div className="hidden md:flex items-baseline gap-1.5">139 <span className="text-base font-bold text-foreground">140 VQuant141 </span>142 </div>143 </button>144 </div>145 </header>146147 {/* Main Content */}148 <main className="flex-1 flex items-center justify-center px-4 py-12 relative z-10">149 <div className="w-full max-w-md">150 {/* Welcome message */}151 <div className="text-center mb-8 animate-in fade-in slide-in-from-top-4 duration-700">152 <h1 className="text-3xl font-bold mb-3 text-foreground">153 Mon Compte154 </h1>155 <p className="text-muted-foreground">156 Créez un compte pour garder vos analyses privées157 </p>158 </div>159160 <Tabs defaultValue="create" className="w-full">161 <TabsList className="grid w-full grid-cols-2 h-12 bg-muted/50 backdrop-blur-sm">162 <TabsTrigger value="create" className="data-[state=active]:bg-background data-[state=active]:shadow-md transition-all">163 Créer un compte164 </TabsTrigger>165 <TabsTrigger value="login" className="data-[state=active]:bg-background data-[state=active]:shadow-md transition-all">166 Se connecter167 </TabsTrigger>168 </TabsList>169170 {/* Create Account Tab */}171 <TabsContent value="create" className="mt-6">172 <Card className="border-border/50 shadow-2xl backdrop-blur-sm bg-card/95">173 <CardHeader className="space-y-3 pb-6">174 <CardTitle className="flex items-center gap-2 text-2xl">175 <div className="p-2 rounded-lg bg-primary/10">176 <User className="w-5 h-5 text-primary" />177 </div>178 Créer un compte179 </CardTitle>180 <CardDescription className="text-base">181 Entrez votre nom pour recevoir un token d'accès unique. Vos générations resteront privées.182 </CardDescription>183 </CardHeader>184 <CardContent className="pt-0">185 <form onSubmit={handleCreate} className="space-y-5">186 <div className="space-y-2">187 <Label htmlFor="create-name" className="text-sm font-medium">Votre nom</Label>188 <div className="relative">189 <User className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />190 <Input191 id="create-name"192 type="text"193 placeholder="Entrez votre nom (min. 2 caractères)"194 value={name}195 onChange={(e) => setName(e.target.value)}196 required197 minLength={2}198 disabled={isLoading || !!generatedToken}199 className="pl-10 h-11 bg-background/50 border-border focus:border-primary transition-all"200 />201 </div>202 </div>203204 {error && (205 <Alert variant="destructive">206 <AlertDescription>{error}</AlertDescription>207 </Alert>208 )}209210 {success && !generatedToken && (211 <Alert>212 <AlertDescription>{success}</AlertDescription>213 </Alert>214 )}215216 {generatedToken && (217 <Alert className="bg-gradient-to-br from-yellow-500/10 to-orange-500/10 border-yellow-500/50 shadow-lg">218 <KeyRound className="w-4 h-4 text-yellow-600 dark:text-yellow-400" />219 <AlertDescription>220 <p className="font-semibold mb-2 text-foreground">221 Votre token d'accès222 </p>223 <div className="flex items-center gap-2">224 <code className="flex-1 bg-background/80 p-3 rounded-lg text-sm break-all select-all border border-yellow-500/30 shadow-inner font-mono">225 {generatedToken}226 </code>227 <Button228 type="button"229 variant="outline"230 size="sm"231 onClick={handleCopyToken}232 className="shrink-0 h-10 w-10 p-0"233 >234 {copied ? (235 <Check className="w-4 h-4 text-green-500" />236 ) : (237 <Copy className="w-4 h-4" />238 )}239 </Button>240 </div>241 <p className="text-xs mt-3 text-destructive font-medium">242 IMPORTANT: Sauvegardez ce token! C'est votre SEUL moyen de vous reconnecter.243 </p>244 <Button245 type="button"246 onClick={handleGoHome}247 className="w-full mt-4 bg-primary hover:bg-primary/90 text-primary-foreground"248 >249 <ArrowLeft className="w-4 h-4 mr-2" />250 Aller à l'accueil251 </Button>252 </AlertDescription>253 </Alert>254 )}255256 {!generatedToken && (257 <Button258 type="submit"259 className="w-full h-11 bg-primary hover:bg-primary/90 text-primary-foreground transition-colors"260 disabled={isLoading}261 >262 {isLoading ? "Création en cours..." : "Créer mon compte"}263 </Button>264 )}265 </form>266 </CardContent>267 </Card>268 </TabsContent>269270 {/* Login Tab */}271 <TabsContent value="login" className="mt-6">272 <Card className="border-border/50 shadow-2xl backdrop-blur-sm bg-card/95">273 <CardHeader className="space-y-3 pb-6">274 <CardTitle className="flex items-center gap-2 text-2xl">275 <div className="p-2 rounded-lg bg-primary/10">276 <KeyRound className="w-5 h-5 text-primary" />277 </div>278 Se connecter279 </CardTitle>280 <CardDescription className="text-base">281 Entrez votre token pour accéder à votre historique privé282 </CardDescription>283 </CardHeader>284 <CardContent className="pt-0">285 <form onSubmit={handleLogin} className="space-y-5">286 <div className="space-y-2">287 <Label htmlFor="login-token" className="text-sm font-medium">Token d'accès</Label>288 <div className="relative">289 <KeyRound className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />290 <Input291 id="login-token"292 type="text"293 placeholder="vquant-xxxxxxx"294 value={loginToken}295 onChange={(e) => setLoginToken(e.target.value)}296 required297 disabled={isLoading}298 className="pl-10 h-11 bg-background/50 border-border focus:border-primary transition-all font-mono"299 />300 </div>301 </div>302303 {error && (304 <Alert variant="destructive">305 <AlertDescription>{error}</AlertDescription>306 </Alert>307 )}308309 {success && (310 <Alert>311 <AlertDescription>{success}</AlertDescription>312 </Alert>313 )}314315 <Button316 type="submit"317 className="w-full h-11 bg-primary hover:bg-primary/90 text-primary-foreground transition-colors"318 disabled={isLoading}319 >320 {isLoading ? "Connexion..." : "Se connecter"}321 </Button>322 </form>323 </CardContent>324 </Card>325 </TabsContent>326 </Tabs>327 </div>328 </main>329 </div>330 );331}332