docs: add comprehensive Stripe billing guide
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 4s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 4s
Covers architecture, configuration steps, user flows, API routes, webhooks, pricing, testing with Stripe CLI, production checklist, and troubleshooting.
This commit is contained in:
@@ -135,7 +135,7 @@ export function UserList({ initialUsers }: { initialUsers: any[] }) {
|
||||
<th className="h-12 px-4 align-middle font-medium text-muted-foreground">{t('admin.users.table.name')}</th>
|
||||
<th className="h-12 px-4 align-middle font-medium text-muted-foreground">{t('admin.users.table.email')}</th>
|
||||
<th className="h-12 px-4 align-middle font-medium text-muted-foreground">{t('admin.users.table.role')}</th>
|
||||
<th className="h-12 px-4 align-middle font-medium text-muted-foreground">{t('admin.users.table.subscription') || 'Abonnement'}</th>
|
||||
<th className="h-12 px-4 align-middle font-medium text-muted-foreground">{t('admin.users.table.subscription')}</th>
|
||||
<th className="h-12 px-4 align-middle font-medium text-muted-foreground">{t('admin.users.table.createdAt')}</th>
|
||||
<th className="h-12 px-4 align-middle font-medium text-muted-foreground text-right">{t('admin.users.table.actions')}</th>
|
||||
</tr>
|
||||
|
||||
@@ -56,7 +56,7 @@ export function AppearanceSettingsClient({
|
||||
localStorage.setItem('theme-preference', next)
|
||||
applyDocumentTheme(next)
|
||||
await updateUserSettings({ theme: next })
|
||||
toast.success(t('settings.settingsSaved') || 'Saved')
|
||||
toast.success(t('settings.settingsSaved'))
|
||||
}
|
||||
|
||||
const handleFontSizeChange = async (value: string) => {
|
||||
@@ -64,7 +64,7 @@ export function AppearanceSettingsClient({
|
||||
const map: Record<string, string> = { small: '14px', medium: '16px', large: '18px', 'extra-large': '20px' }
|
||||
document.documentElement.style.setProperty('--user-font-size', map[value] || '16px')
|
||||
await updateAISettings({ fontSize: value as any })
|
||||
toast.success(t('settings.settingsSaved') || 'Saved')
|
||||
toast.success(t('settings.settingsSaved'))
|
||||
}
|
||||
|
||||
const handleFontFamilyChange = async (value: string) => {
|
||||
@@ -80,7 +80,7 @@ export function AppearanceSettingsClient({
|
||||
if (font === 'playfair') root.classList.add('font-playfair')
|
||||
if (font === 'jetbrains') root.classList.add('font-jetbrains')
|
||||
await updateAISettings({ fontFamily: font as 'inter' | 'playfair' | 'jetbrains' | 'system' })
|
||||
toast.success(t('settings.settingsSaved') || 'Saved')
|
||||
toast.success(t('settings.settingsSaved'))
|
||||
}
|
||||
|
||||
const SelectCard = ({
|
||||
@@ -174,7 +174,7 @@ export function AppearanceSettingsClient({
|
||||
>
|
||||
<div className="space-y-10">
|
||||
<p className="text-[11px] font-bold uppercase tracking-[0.2em] text-muted-foreground">
|
||||
{t('appearance.description') || "Personnalisez l'interface"}
|
||||
{t('appearance.description')}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
@@ -186,8 +186,8 @@ export function AppearanceSettingsClient({
|
||||
<Palette size={20} />
|
||||
</div>
|
||||
<div className="space-y-0.5 text-left">
|
||||
<h4 className="text-base font-bold text-foreground">{t('appearance.accentColorTitle') || 'Couleur d\'accentuation'}</h4>
|
||||
<p className="text-[11px] text-muted-foreground leading-tight">{t('appearance.accentColorDescription') || 'Définissez la couleur principale de votre espace de travail'}</p>
|
||||
<h4 className="text-base font-bold text-foreground">{t('appearance.accentColorTitle')}</h4>
|
||||
<p className="text-[11px] text-muted-foreground leading-tight">{t('appearance.accentColorDescription')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 bg-slate-50 dark:bg-black/20 px-4 py-2 rounded-xl border border-border/40">
|
||||
@@ -264,14 +264,14 @@ export function AppearanceSettingsClient({
|
||||
|
||||
<SelectCard
|
||||
icon={Type}
|
||||
title={t('appearance.fontFamilyLabel') || 'Police'}
|
||||
description={t('appearance.fontFamilyDescription') || "Choisissez la police de l'application"}
|
||||
title={t('appearance.fontFamilyLabel')}
|
||||
description={t('appearance.fontFamilyDescription')}
|
||||
value={fontFamily}
|
||||
options={[
|
||||
{ value: 'inter', label: t('appearance.fontInterDefault') },
|
||||
{ value: 'playfair', label: t('appearance.fontPlayfairDisplay') },
|
||||
{ value: 'jetbrains', label: t('appearance.fontJetBrainsMono') },
|
||||
{ value: 'system', label: t('appearance.fontSystem') || 'Système' },
|
||||
{ value: 'system', label: t('appearance.fontSystem') },
|
||||
]}
|
||||
onChange={handleFontFamilyChange}
|
||||
/>
|
||||
|
||||
@@ -32,12 +32,12 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
|
||||
if (value === 'auto') {
|
||||
localStorage.removeItem('user-language')
|
||||
document.cookie = 'user-language=;path=/;max-age=0'
|
||||
toast.success(t('settings.languageAuto') || 'Language set to Auto')
|
||||
toast.success(t('settings.languageAuto'))
|
||||
} else {
|
||||
localStorage.setItem('user-language', value)
|
||||
document.cookie = `user-language=${value};path=/;max-age=${60 * 60 * 24 * 365};samesite=lax`
|
||||
setContextLanguage(value as any)
|
||||
toast.success(t('profile.languageUpdateSuccess') || 'Language updated')
|
||||
toast.success(t('profile.languageUpdateSuccess'))
|
||||
}
|
||||
setTimeout(() => router.refresh(), 300)
|
||||
}
|
||||
@@ -45,19 +45,19 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
|
||||
const handleEmailNotificationsChange = async (enabled: boolean) => {
|
||||
setEmailNotifications(enabled)
|
||||
await updateAISettings({ emailNotifications: enabled })
|
||||
toast.success(t('settings.settingsSaved') || 'Saved')
|
||||
toast.success(t('settings.settingsSaved'))
|
||||
}
|
||||
|
||||
const handleDesktopNotificationsChange = async (enabled: boolean) => {
|
||||
setDesktopNotifications(enabled)
|
||||
await updateAISettings({ desktopNotifications: enabled })
|
||||
toast.success(t('settings.settingsSaved') || 'Saved')
|
||||
toast.success(t('settings.settingsSaved'))
|
||||
}
|
||||
|
||||
const handleAutoSaveChange = async (enabled: boolean) => {
|
||||
setAutoSave(enabled)
|
||||
await updateAISettings({ autoSave: enabled })
|
||||
toast.success(t('settings.settingsSaved') || 'Saved')
|
||||
toast.success(t('settings.settingsSaved'))
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -89,21 +89,21 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
|
||||
className="w-full bg-white/50 dark:bg-black/40 border border-border rounded-xl px-5 py-3.5 text-sm outline-none focus:ring-1 ring-brand-accent/20 appearance-none cursor-pointer transition-all hover:bg-white dark:hover:bg-black/60 text-ink font-medium"
|
||||
>
|
||||
<option value="auto">{t('profile.autoDetect')}</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="es">Español</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="fa">فارسی</option>
|
||||
<option value="it">Italiano</option>
|
||||
<option value="pt">Português</option>
|
||||
<option value="ru">Русский</option>
|
||||
<option value="zh">中文</option>
|
||||
<option value="ja">日本語</option>
|
||||
<option value="ko">한국어</option>
|
||||
<option value="ar">العربية</option>
|
||||
<option value="hi">हिन्दी</option>
|
||||
<option value="nl">Nederlands</option>
|
||||
<option value="pl">Polski</option>
|
||||
<option value="en">{t('languages.en')}</option>
|
||||
<option value="fr">{t('languages.fr')}</option>
|
||||
<option value="es">{t('languages.es')}</option>
|
||||
<option value="de">{t('languages.de')}</option>
|
||||
<option value="fa">{t('languages.fa')}</option>
|
||||
<option value="it">{t('languages.it')}</option>
|
||||
<option value="pt">{t('languages.pt')}</option>
|
||||
<option value="ru">{t('languages.ru')}</option>
|
||||
<option value="zh">{t('languages.zh')}</option>
|
||||
<option value="ja">{t('languages.ja')}</option>
|
||||
<option value="ko">{t('languages.ko')}</option>
|
||||
<option value="ar">{t('languages.ar')}</option>
|
||||
<option value="hi">{t('languages.hi')}</option>
|
||||
<option value="nl">{t('languages.nl')}</option>
|
||||
<option value="pl">{t('languages.pl')}</option>
|
||||
</select>
|
||||
<div className="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none opacity-40 text-concrete">
|
||||
<svg width="10" height="6" viewBox="0 0 10 6" fill="none">
|
||||
@@ -159,8 +159,8 @@ export function GeneralSettingsClient({ initialSettings }: GeneralSettingsClient
|
||||
|
||||
<div className="flex items-center justify-between pt-6">
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-bold text-ink">{t('settings.autoSave') || 'Auto-Save'}</p>
|
||||
<p className="text-[10px] text-concrete leading-relaxed">{t('settings.autoSaveDesc') || 'Sauvegarder automatiquement les modifications'}</p>
|
||||
<p className="text-xs font-bold text-ink">{t('settings.autoSave')}</p>
|
||||
<p className="text-[10px] text-concrete leading-relaxed">{t('settings.autoSaveDesc')}</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
|
||||
@@ -121,14 +121,14 @@ export function ProfileForm({ user }: { user: { name: string | null; email: stri
|
||||
{/* Preferences */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-[10px] font-bold uppercase tracking-[0.2em] text-concrete opacity-60">
|
||||
{t('profile.preferences') || 'Préférences de compte'}
|
||||
{t('profile.preferences')}
|
||||
</h4>
|
||||
<div className="p-4 bg-white/40 dark:bg-white/5 border border-border rounded-2xl flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Bell size={18} className="text-concrete" />
|
||||
<div>
|
||||
<p className="text-sm text-ink">{t('profile.desktopNotifications') || 'Notification bureau'}</p>
|
||||
<p className="text-[10px] text-concrete font-light pe-4">{t('profile.desktopNotificationsDesc') || 'Recevez des alertes pour vos rappels et activités IA.'}</p>
|
||||
<p className="text-sm text-ink">{t('profile.desktopNotifications')}</p>
|
||||
<p className="text-[10px] text-concrete font-light pe-4">{t('profile.desktopNotificationsDesc')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -147,7 +147,7 @@ export function ProfileForm({ user }: { user: { name: string | null; email: stri
|
||||
className="flex items-center gap-3 px-6 py-3 bg-rose-50 dark:bg-rose-500/10 text-rose-600 rounded-xl font-bold uppercase tracking-widest text-[10px] hover:bg-rose-100 transition-colors"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
{t('sidebar.signOut') || 'Déconnexion'}
|
||||
{t('sidebar.signOut')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,7 @@ export function TrashClient({
|
||||
const all: TrashItem[] = [
|
||||
...notes.map(n => ({
|
||||
id: n.id,
|
||||
title: n.title || t('notes.untitled') || 'Untitled',
|
||||
title: n.title || t('notes.untitled'),
|
||||
itemType: 'note' as const,
|
||||
deletedAt: (n as any).trashedAt || null,
|
||||
content: n.content,
|
||||
@@ -101,9 +101,9 @@ export function TrashClient({
|
||||
await restoreNb(item.id)
|
||||
setNotebooks(prev => prev.filter((nb: any) => nb.id !== item.id))
|
||||
}
|
||||
toast.success(t('trash.restoreSuccess') || 'Restored successfully')
|
||||
toast.success(t('trash.restoreSuccess'))
|
||||
} catch {
|
||||
toast.error(t('trash.restoreError') || 'Failed to restore')
|
||||
toast.error(t('trash.restoreError'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,22 +116,22 @@ export function TrashClient({
|
||||
await permDeleteNb(item.id)
|
||||
setNotebooks(prev => prev.filter((nb: any) => nb.id !== item.id))
|
||||
}
|
||||
toast.success(t('trash.permanentDeleteSuccess') || 'Permanently deleted')
|
||||
toast.success(t('trash.permanentDeleteSuccess'))
|
||||
} catch {
|
||||
toast.error(t('trash.deleteError') || 'Failed to delete')
|
||||
toast.error(t('trash.deleteError'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleEmptyTrash = async () => {
|
||||
if (!window.confirm(t('trash.emptyTrashConfirm') || 'Empty trash? This is irreversible.')) return
|
||||
if (!window.confirm(t('trash.emptyTrashConfirm'))) return
|
||||
setIsEmptying(true)
|
||||
try {
|
||||
await emptyTrash()
|
||||
setNotes([])
|
||||
setNotebooks([])
|
||||
toast.success(t('trash.emptyTrashSuccess') || 'Trash emptied')
|
||||
toast.success(t('trash.emptyTrashSuccess'))
|
||||
} catch {
|
||||
toast.error(t('trash.deleteError') || 'Failed to empty trash')
|
||||
toast.error(t('trash.deleteError'))
|
||||
} finally {
|
||||
setIsEmptying(false)
|
||||
}
|
||||
@@ -143,10 +143,10 @@ export function TrashClient({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-4xl font-memento-serif font-medium text-foreground flex items-center gap-4">
|
||||
{t('sidebar.trash') || 'Trash'} <Trash2 size={28} className="text-rose-400 opacity-40" />
|
||||
{t('sidebar.trash')} <Trash2 size={28} className="text-rose-400 opacity-40" />
|
||||
</h1>
|
||||
<p className="text-[10px] text-muted-foreground font-bold uppercase tracking-[0.3em] opacity-60">
|
||||
{t('trash.autoDelete30') || 'Auto-delete after 30 days'}
|
||||
{t('trash.autoDelete30')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -156,7 +156,7 @@ export function TrashClient({
|
||||
disabled={isEmptying}
|
||||
className="px-6 py-3 bg-card border border-border text-rose-500 rounded-2xl text-[10px] font-bold uppercase tracking-widest hover:bg-rose-50 hover:border-rose-100 transition-all shadow-sm disabled:opacity-50"
|
||||
>
|
||||
{t('trash.emptyTrash') || 'Empty all'}
|
||||
{t('trash.emptyTrash')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -166,7 +166,7 @@ export function TrashClient({
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground group-focus-within:text-foreground transition-colors" size={16} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('common.search') || 'Search...'}
|
||||
placeholder={t('common.search')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-white dark:bg-white/5 border border-border/40 rounded-2xl pl-12 pr-6 py-4 text-sm outline-none focus:ring-4 ring-foreground/5 transition-all shadow-sm"
|
||||
@@ -181,7 +181,7 @@ export function TrashClient({
|
||||
className={`px-6 py-3 rounded-xl text-[10px] font-bold uppercase tracking-widest transition-all
|
||||
${filterType === type ? 'bg-foreground text-background shadow-lg' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
>
|
||||
{type === 'all' ? (t('trash.filterAll') || 'All') : type === 'notes' ? (t('nav.notes') || 'Notes') : (t('nav.notebooks') || 'Notebooks')}
|
||||
{type === 'all' ? t('trash.filterAll') : type === 'notes' ? t('nav.notes') : t('nav.notebooks')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -220,12 +220,12 @@ export function TrashClient({
|
||||
onClick={() => handleRestore(item)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-emerald-50 text-emerald-600 rounded-xl text-[10px] font-bold uppercase tracking-widest hover:bg-emerald-100 transition-colors"
|
||||
>
|
||||
<RotateCcw size={12} /> {t('trash.restore') || 'Restore'}
|
||||
<RotateCcw size={12} /> {t('trash.restore')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePermanentDelete(item)}
|
||||
className="p-2 hover:bg-rose-50 text-rose-500 rounded-xl transition-colors"
|
||||
title={t('trash.permanentDelete') || 'Delete permanently'}
|
||||
title={t('trash.permanentDelete')}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
@@ -238,7 +238,7 @@ export function TrashClient({
|
||||
</h3>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`text-[9px] font-bold uppercase tracking-widest px-2 py-0.5 rounded border ${daysLeft < 5 ? 'border-rose-200 text-rose-500 bg-rose-50' : 'border-terreo/20 text-terreo bg-terreo/5'}`}>
|
||||
{daysLeft} {t('trash.daysRemaining') || 'DAYS LEFT'}
|
||||
{daysLeft} {t('trash.daysRemaining')}
|
||||
</div>
|
||||
{item.deletedAt && (
|
||||
<span className="text-[10px] text-muted-foreground font-medium uppercase tracking-tight flex items-center gap-1">
|
||||
@@ -255,7 +255,7 @@ export function TrashClient({
|
||||
) : (
|
||||
<div className="border-t border-border/40 pt-4">
|
||||
<div className="text-[9px] font-bold text-muted-foreground/40 uppercase tracking-widest">
|
||||
{t('trash.notebookContentPreserved') || 'Notebook content preserved'}
|
||||
{t('trash.notebookContentPreserved')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -271,10 +271,10 @@ export function TrashClient({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-2xl font-memento-serif text-foreground italic">
|
||||
{t('trash.empty') || 'Trash is empty'}
|
||||
{t('trash.empty')}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground max-w-xs">
|
||||
{t('trash.emptyDescription') || 'Deleted items will appear here. They are kept for 30 days before permanent deletion.'}
|
||||
{t('trash.emptyDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,7 +284,7 @@ export function TrashClient({
|
||||
<footer className="px-12 py-6 bg-white/50 dark:bg-white/5 border-t border-border flex items-center gap-4">
|
||||
<AlertCircle size={14} className="text-muted-foreground" />
|
||||
<p className="text-[10px] text-muted-foreground font-medium uppercase tracking-widest">
|
||||
{t('trash.notebookRestoreHint') || 'Restoring a notebook also restores all its notes.'}
|
||||
{t('trash.notebookRestoreHint')}
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user