feat: dashboard Second Brain, essai 7 jours et vérification e-mail
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m14s
CI / Deploy production (on server) (push) Successful in 1m25s

Rendre le dashboard actionnable (inbox, peek, carte mentale), aligner la facturation sur l’essai 7 jours, et bloquer le login e-mail tant que l’adresse n’est pas confirmée.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antigravity
2026-08-30 07:19:36 +00:00
parent 69c99e4f4f
commit 80ccc1f6de
95 changed files with 4158 additions and 618 deletions

View File

@@ -0,0 +1,92 @@
'use client'
import { Suspense, useState } from 'react'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import { Mail, ArrowLeft, Sparkles } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { resendSignupVerification } from '@/app/actions/auth-verify'
import { toast } from 'sonner'
function CheckEmailContent() {
const { t, language } = useLanguage()
const searchParams = useSearchParams()
const initialEmail = searchParams.get('email') ?? ''
const [email, setEmail] = useState(initialEmail)
const [sending, setSending] = useState(false)
const handleResend = async () => {
const target = email.trim()
if (!target) {
toast.error(t('auth.verifyMissingEmail'))
return
}
setSending(true)
const result = await resendSignupVerification(target, language)
setSending(false)
if (result.success) {
toast.success(t('auth.verifyResent'))
} else {
toast.error(t('auth.verifyResendFailed'))
}
}
return (
<div className="bg-white dark:bg-[var(--background)]/50 border border-[var(--border)] p-8 md:p-10 rounded-[48px] shadow-2xl">
<div className="space-y-8 text-center">
<div className="w-14 h-14 mx-auto rounded-2xl bg-[var(--color-brand-accent)]/10 flex items-center justify-center">
<Mail size={28} className="text-[var(--color-brand-accent)]" />
</div>
<div className="space-y-2">
<h1 className="text-2xl md:text-3xl font-serif font-bold">
{t('auth.checkEmailTitle')}
</h1>
<p className="text-[var(--muted-foreground)] text-sm font-light leading-relaxed">
{initialEmail
? t('auth.checkEmailDescription', { email: initialEmail })
: t('auth.checkEmailDescriptionGeneric')}
</p>
</div>
<div className="space-y-1.5 text-start">
<label htmlFor="email" className="text-[10px] uppercase tracking-widest font-bold text-[var(--muted-foreground)] px-4">
{t('auth.email')}
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t('auth.emailPlaceholder')}
className="w-full bg-slate-50 dark:bg-white/5 border border-[var(--border)] rounded-2xl py-4 px-4 text-sm outline-none focus:border-[var(--color-brand-accent)] focus:ring-4 ring-[var(--color-brand-accent)]/5 transition-all"
/>
</div>
<button
type="button"
onClick={handleResend}
disabled={sending}
className="w-full bg-[var(--foreground)] text-[var(--background)] py-4 rounded-2xl font-bold uppercase tracking-[0.2em] text-[10px] flex items-center justify-center gap-3 transition-all hover:shadow-xl hover:shadow-black/10 active:scale-[0.98] disabled:opacity-50"
>
{sending ? <Sparkles size={16} className="animate-spin" /> : t('auth.resendVerification')}
</button>
<Link
href="/login"
className="inline-flex items-center gap-2 text-xs text-[var(--muted-foreground)] hover:text-[var(--color-brand-accent)] transition-colors"
>
<ArrowLeft size={14} />
{t('auth.backToLogin')}
</Link>
</div>
</div>
)
}
export default function CheckEmailPage() {
return (
<Suspense fallback={<div className="p-10 text-center text-sm text-muted-foreground"></div>}>
<CheckEmailContent />
</Suspense>
)
}

View File

@@ -0,0 +1,97 @@
'use client'
import { Suspense, useEffect, useState } from 'react'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
import { CheckCircle2, AlertCircle, Sparkles } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { confirmEmail } from '@/app/actions/auth-verify'
function VerifyEmailContent() {
const { t } = useLanguage()
const searchParams = useSearchParams()
const token = searchParams.get('token')
const [status, setStatus] = useState<'loading' | 'ok' | 'invalid' | 'expired'>('loading')
useEffect(() => {
if (!token) {
setStatus('invalid')
return
}
let cancelled = false
;(async () => {
const result = await confirmEmail(token)
if (cancelled) return
if (result.success) setStatus('ok')
else setStatus(result.error === 'expired' ? 'expired' : 'invalid')
})()
return () => {
cancelled = true
}
}, [token])
if (status === 'loading') {
return (
<div className="bg-white dark:bg-[var(--background)]/50 border border-[var(--border)] p-8 md:p-10 rounded-[48px] shadow-2xl text-center space-y-4">
<Sparkles size={28} className="mx-auto animate-spin text-[var(--color-brand-accent)]" />
<p className="text-sm text-[var(--muted-foreground)]">{t('auth.verifyLoading')}</p>
</div>
)
}
if (status === 'ok') {
return (
<div className="bg-white dark:bg-[var(--background)]/50 border border-[var(--border)] p-8 md:p-10 rounded-[48px] shadow-2xl text-center space-y-6">
<div className="w-14 h-14 mx-auto rounded-2xl bg-emerald-500/10 flex items-center justify-center">
<CheckCircle2 size={28} className="text-emerald-600" />
</div>
<div className="space-y-2">
<h1 className="text-2xl font-serif font-bold">{t('auth.verifySuccessTitle')}</h1>
<p className="text-sm text-[var(--muted-foreground)]">{t('auth.verifySuccessDescription')}</p>
</div>
<Link href="/login?verified=1" className="block">
<button
type="button"
className="w-full bg-[var(--foreground)] text-[var(--background)] py-4 rounded-2xl font-bold uppercase tracking-[0.2em] text-[10px] transition-all hover:shadow-xl active:scale-[0.98]"
>
{t('auth.signIn')}
</button>
</Link>
</div>
)
}
return (
<div className="bg-white dark:bg-[var(--background)]/50 border border-[var(--border)] p-8 md:p-10 rounded-[48px] shadow-2xl text-center space-y-6">
<div className="w-14 h-14 mx-auto rounded-2xl bg-red-500/10 flex items-center justify-center">
<AlertCircle size={28} className="text-red-500" />
</div>
<div className="space-y-2">
<h1 className="text-2xl font-serif font-bold">
{status === 'expired' ? t('auth.verifyExpiredTitle') : t('auth.verifyInvalidTitle')}
</h1>
<p className="text-sm text-[var(--muted-foreground)]">
{status === 'expired'
? t('auth.verifyExpiredDescription')
: t('auth.verifyInvalidDescription')}
</p>
</div>
<Link href="/check-email" className="block">
<button
type="button"
className="w-full bg-[var(--foreground)] text-[var(--background)] py-4 rounded-2xl font-bold uppercase tracking-[0.2em] text-[10px] transition-all hover:shadow-xl active:scale-[0.98]"
>
{t('auth.resendVerification')}
</button>
</Link>
</div>
)
}
export default function VerifyEmailPage() {
return (
<Suspense fallback={<div className="p-10 text-center text-sm text-muted-foreground"></div>}>
<VerifyEmailContent />
</Suspense>
)
}