SPB Git

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%
13.3 KB · 332 lines tsx
Raw Blame History
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