docs: add comprehensive Stripe billing guide
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:
Antigravity
2026-05-16 21:10:26 +00:00
parent aa12d2226f
commit bb75b2e763
36 changed files with 2099 additions and 735 deletions

View File

@@ -58,13 +58,9 @@ export function AIChat({ showFloatingTrigger = true }: { showFloatingTrigger?: b
if (parsed.error === 'QUOTA_EXCEEDED') {
const isBasic = (parsed.currentTier || 'BASIC') === 'BASIC'
toast.error(
language === 'fr'
? isBasic
? 'Le chat IA est réservé au plan PRO et supérieur.'
: `Limite mensuelle atteinte pour le plan ${parsed.currentTier}. Elle se réinitialise le mois prochain.`
: isBasic
? 'AI Chat is available from the PRO plan onwards.'
: `Monthly quota reached for ${parsed.currentTier} plan. It will reset next month.`,
isBasic
? t('chat.quotaExceededBasic')
: t('chat.quotaExceededTier', { tier: parsed.currentTier }),
{ duration: 8000 }
)
return

View File

@@ -40,7 +40,7 @@ function getActionLabel(action: string, t: (key: string) => string | undefined):
function timeAgo(dateStr: string, t: (key: string) => string | undefined): string {
const diff = Date.now() - new Date(dateStr).getTime()
const mins = Math.floor(diff / 60000)
if (mins < 1) return t('brainstorm.justNow') || 'just now'
if (mins < 1) return t('brainstorm.justNow')
if (mins < 60) return `${mins}m`
const hours = Math.floor(mins / 60)
if (hours < 24) return `${hours}h`
@@ -63,7 +63,7 @@ export function ActivityFeed({ activities, isOpen, onToggle, t }: ActivityFeedPr
<div className="flex items-center gap-2">
<Activity size={14} className="text-orange-500" />
<h3 className="text-[10px] font-bold uppercase tracking-[0.2em] text-foreground">
{t('brainstorm.activityTitle') || 'Activity'}
{t('brainstorm.activityTitle')}
</h3>
</div>
<button
@@ -78,7 +78,7 @@ export function ActivityFeed({ activities, isOpen, onToggle, t }: ActivityFeedPr
{activities.length === 0 ? (
<div className="p-6 text-center">
<p className="text-xs italic text-muted-foreground">
{t('brainstorm.noActivity') || 'No activity yet'}
{t('brainstorm.noActivity')}
</p>
</div>
) : (

View File

@@ -289,7 +289,7 @@ export function BrainstormPage() {
</motion.div>
<div className="flex-1">
<h1 className="text-4xl font-serif font-medium text-foreground tracking-tight">
{t('brainstorm.title') || 'Waves of Thought'}
{t('brainstorm.title')}
</h1>
<div className="flex items-center gap-2 mt-1">
<span className="w-8 h-px bg-brand-accent/40" />

View File

@@ -185,7 +185,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
handleNoteCreated(newNote)
setEditingNote({ note: newNote, readOnly: false })
} catch {
toast.error(t('notes.createFailed') || 'Failed to create note')
toast.error(t('notes.createFailed'))
}
})
}
@@ -429,10 +429,10 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
}, [sortedNotes])
const sortLabels: Record<SortOrder, string> = {
newest: t('sidebar.sortNewest') || 'Plus récentes',
oldest: t('sidebar.sortOldest') || 'Plus anciennes',
alpha: t('sidebar.sortAlpha') || 'A → Z',
manual: t('sidebar.sortManual') || 'Libre',
newest: t('sidebar.sortNewest'),
oldest: t('sidebar.sortOldest'),
alpha: t('sidebar.sortAlpha'),
manual: t('sidebar.sortManual'),
}
@@ -495,9 +495,9 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
{currentNotebook
? currentNotebook.name
: searchParams.get('shared') === '1'
? (t('sidebar.sharedWithMe') || 'Partagées avec moi')
? t('sidebar.sharedWithMe')
: searchParams.get('reminders') === '1'
? (t('sidebar.reminders') || 'Rappels')
? t('sidebar.reminders')
: t('notes.title')}
</h1>
</div>
@@ -511,7 +511,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
className="flex items-center gap-2 text-[13px] text-foreground font-medium hover:opacity-70 transition-opacity"
>
<Plus size={16} />
<span>{t('notes.newNote') || 'Add Note'}</span>
<span>{t('notes.newNote')}</span>
</button>
{currentNotebook && (
@@ -520,7 +520,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
className="flex items-center gap-2 text-[13px] text-foreground font-medium hover:opacity-70 transition-opacity"
>
<FolderOpen size={16} />
<span>{t('notebook.createSubNotebook') || 'Nouveau sous-carnet'}</span>
<span>{t('notebook.createSubNotebook')}</span>
</button>
)}
@@ -558,7 +558,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
router.push(`/home?${params.toString()}`)
}
}}
placeholder={t('search.placeholder') || 'Rechercher...'}
placeholder={t('search.placeholder')}
className="w-48 bg-transparent border-b border-foreground/20 focus:border-foreground outline-none text-[13px] text-foreground placeholder:text-muted-foreground/50 py-0.5 transition-colors"
/>
{inlineSearchQuery && (
@@ -585,7 +585,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
className="flex items-center gap-2 text-[13px] text-foreground font-medium hover:opacity-70 transition-opacity"
>
<Search size={16} />
<span>{t('notes.search') || 'Search'}</span>
<span>{t('notes.search')}</span>
</button>
)}
@@ -595,7 +595,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
className="flex items-center gap-2 text-[13px] text-foreground font-medium hover:opacity-70 transition-opacity"
>
<Sparkles size={16} />
<span>{t('notes.reorganize') || 'Réorganiser les notes'}</span>
<span>{t('notes.reorganize')}</span>
</button>
)}
@@ -622,7 +622,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
title={initialSettings.aiAssistantEnabled ? t('notebook.summary') : t('notebook.assistantRequiredForSummarize')}
>
<FileText size={16} />
<span>{t('notebook.summary') || 'Summarize'}</span>
<span>{t('notebook.summary')}</span>
</button>
)}
<button
@@ -640,7 +640,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 text-[10px] font-bold uppercase tracking-[0.2em] text-muted-foreground">
<TagIcon size={12} />
<span>{t('labels.filterByTags') || 'Filter by Tags'}</span>
<span>{t('labels.filterByTags')}</span>
{selectedTagIds.length > 0 && (
<span className="bg-brand-accent/10 text-brand-accent px-2 py-0.5 rounded-full text-[9px] lowercase tracking-normal">
{selectedTagIds.length} active
@@ -650,7 +650,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
{availableTags.length > 10 && (
<input
type="text"
placeholder={t('labels.searchTags') || 'Search tags...'}
placeholder={t('labels.searchTags')}
className="bg-transparent border-b border-foreground/10 text-[10px] outline-none focus:border-brand-accent/40 py-1 px-2 w-32 transition-all focus:w-48 placeholder:text-muted-foreground/40"
value={tagSearchQuery}
onChange={e => setTagSearchQuery(e.target.value)}
@@ -696,7 +696,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
className="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60 hover:text-foreground transition-colors border border-dashed border-border rounded-full"
>
{isTagsExpanded
? (t('labels.showLess') || 'Show less')
? t('labels.showLess')
: `+ ${availableTags.length - 10} more`}
</button>
)}
@@ -706,7 +706,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
onClick={() => setSelectedTagIds([])}
className="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider text-red-500 hover:underline ms-auto"
>
{t('labels.clearAll') || 'Clear all'}
{t('labels.clearAll')}
</button>
)}
</div>
@@ -745,14 +745,14 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
{notes.length === 0 && (
<div className="h-64 flex flex-col items-center justify-center text-center space-y-4">
<p className="font-memento-serif text-xl italic text-muted-foreground">
{t('notes.emptyState') || 'This notebook is waiting for its first vision.'}
{t('notes.emptyState')}
</p>
<button
onClick={handleAddNote}
disabled={isCreating}
className="px-6 py-2 border border-foreground text-[13px] uppercase tracking-[0.2em] hover:bg-foreground hover:text-background transition-all"
>
{t('notes.createFirst') || 'Begin Drawing'}
{t('notes.createFirst')}
</button>
</div>
)}

View File

@@ -0,0 +1,126 @@
'use client'
import { useEffect, useState } from 'react'
import { useLanguage } from '@/lib/i18n'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { getConsent, setConsent } from '@/lib/consent/cookie-consent'
import { toast } from 'sonner'
interface CookiePreferencesDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function CookiePreferencesDialog({ open, onOpenChange }: CookiePreferencesDialogProps) {
const { t } = useLanguage()
const [analytics, setAnalytics] = useState(false)
useEffect(() => {
if (!open) return
const current = getConsent()
setAnalytics(current?.analytics ?? false)
}, [open])
const handleSave = () => {
setConsent({ analytics, marketing: false })
toast.success(t('consent.preferences.saved'))
onOpenChange(false)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg border-border bg-memento-paper dark:bg-background">
<DialogHeader>
<DialogTitle className="font-memento-serif text-xl text-ink">
{t('consent.preferences.title')}
</DialogTitle>
<DialogDescription className="text-[11px] text-concrete leading-relaxed">
{t('consent.preferences.description')}
</DialogDescription>
</DialogHeader>
<motionless className="space-y-6 py-2">
<CategoryRow
title={t('consent.preferences.necessaryTitle')}
description={t('consent.preferences.necessaryDesc')}
locked
checked
/>
<CategoryRow
title={t('consent.preferences.analyticsTitle')}
description={t('consent.preferences.analyticsDesc')}
checked={analytics}
onChange={setAnalytics}
/>
</motionless>
<DialogFooter className="gap-2 sm:gap-2">
<button
type="button"
onClick={() => onOpenChange(false)}
className="px-5 py-2.5 text-[10px] font-bold uppercase tracking-[0.2em] text-concrete hover:text-ink transition-colors"
>
{t('common.cancel')}
</button>
<button
type="button"
onClick={handleSave}
className="px-6 py-2.5 bg-foreground text-background rounded-xl text-[10px] font-bold uppercase tracking-[0.2em] hover:opacity-90 transition-opacity"
>
{t('consent.preferences.save')}
</button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
function CategoryRow({
title,
description,
checked,
onChange,
locked,
}: {
title: string
description: string
checked: boolean
onChange?: (v: boolean) => void
locked?: boolean
}) {
return (
<motionless className="flex items-start justify-between gap-4 border-b border-border/40 pb-5 last:border-0 last:pb-0">
<div className="space-y-1 pe-4">
<p className="text-xs font-bold text-ink">{title}</p>
<p className="text-[10px] text-concrete leading-relaxed">{description}</p>
{locked && (
<p className="text-[9px] font-bold uppercase tracking-widest text-concrete/80">
{title.includes('—') ? '' : ''}
</p>
)}
</motionless>
<label className={`relative inline-flex shrink-0 items-center ${locked ? 'opacity-50 pointer-events-none' : 'cursor-pointer'}`}>
<input
type="checkbox"
className="sr-only peer"
checked={checked}
disabled={locked}
onChange={(e) => onChange?.(e.target.checked)}
/>
<motionless className="w-11 h-6 bg-gray-200 dark:bg-white/10 rounded-full peer peer-checked:after:translate-x-[20px] after:content-[''] after:absolute after:top-[4px] after:left-[4px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-ink" />
</label>
</motionless>
)
}
/** Avoid pulling motion into dialog — plain wrapper */
function motionless({ children, className }: { children: React.ReactNode; className?: string }) {
return <motionless className={className}>{children}</motionless>
}

View File

@@ -234,7 +234,7 @@ export function NoteActions({
<DropdownMenuItem onClick={onOpenHistory}>
<History className="h-4 w-4 mr-2" />
{historyEnabled
? (t('notes.history') || 'Historique')
? t('notes.history')
: (t('notes.enableHistory') || "Activer l'historique")}
</DropdownMenuItem>
)}

View File

@@ -351,9 +351,9 @@ export const NoteCard = memo(function NoteCard({
refreshNotes(note?.notebookId)
if (!note.isPinned) {
toast.success(t('notes.pinned') || 'Note pinned')
toast.success(t('notes.pinned'))
} else {
toast.info(t('notes.unpinned') || 'Note unpinned')
toast.info(t('notes.unpinned'))
}
})
}
@@ -483,8 +483,8 @@ export const NoteCard = memo(function NoteCard({
{/* Drag Handle - Only visible on mobile/touch devices */}
<div
className="muuri-drag-handle absolute top-2 start-2 z-20 cursor-grab active:cursor-grabbing p-2 md:hidden"
aria-label={t('notes.dragToReorder') || 'Drag to reorder'}
title={t('notes.dragToReorder') || 'Drag to reorder'}
aria-label={t('notes.dragToReorder')}
title={t('notes.dragToReorder')}
>
<GripVertical className="h-5 w-5 text-muted-foreground" />
</div>
@@ -563,7 +563,7 @@ export const NoteCard = memo(function NoteCard({
<DropdownMenuSeparator />
<DropdownMenuItem className="text-muted-foreground" onSelect={() => onCreateSubNotebook?.()}>
<Plus className="h-4 w-4 me-2" />
{t('notebook.createSubNotebook') || 'Nouveau sous-carnet…'}
{t('notebook.createSubNotebook')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -605,7 +605,7 @@ export const NoteCard = memo(function NoteCard({
<button
onClick={handleRemoveFusedBadge}
className="ms-1 opacity-0 group-hover/badge:opacity-100 hover:opacity-100 transition-opacity"
title={t('notes.remove') || 'Remove'}
title={t('notes.remove')}
>
<Trash2 className="h-2.5 w-2.5" />
</button>
@@ -873,13 +873,13 @@ export const NoteCard = memo(function NoteCard({
<AlertDialogHeader>
<AlertDialogTitle>{t('notes.confirmDeleteTitle') || t('notes.delete')}</AlertDialogTitle>
<AlertDialogDescription>
{t('notes.confirmDelete') || 'Are you sure you want to delete this note?'}
{t('notes.confirmDelete')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('common.cancel') || 'Cancel'}</AlertDialogCancel>
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={handleDelete}>
{t('notes.delete') || 'Delete'}
{t('notes.delete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View File

@@ -48,6 +48,7 @@ function fmtDate(date: Date | string, language: string): string {
}
function VersionPreview({ entry, language }: { entry: NoteHistoryEntry; language: string }) {
const { t } = useLanguage()
const isMd = entry.type === 'markdown' || entry.isMarkdown
const isCl = entry.type === 'checklist'
const isRt = entry.type === 'richtext'
@@ -56,15 +57,15 @@ function VersionPreview({ entry, language }: { entry: NoteHistoryEntry; language
<div className="space-y-4">
<div>
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-1">
{language === 'fr' ? 'Titre' : 'Title'}
{t('noteHistory.title')}
</p>
<p className="text-sm text-foreground font-medium">
{entry.title || (language === 'fr' ? 'Sans titre' : 'Untitled')}
{entry.title || t('noteHistory.untitled')}
</p>
</div>
<div>
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-1">
{language === 'fr' ? 'Contenu' : 'Content'}
{t('noteHistory.content')}
</p>
<div className="rounded-md border border-border/70 bg-muted/30 p-3 text-sm text-foreground overflow-auto max-h-[48vh]">
{isCl && entry.checkItems ? (

View File

@@ -222,7 +222,7 @@ export function BillingPlans() {
buttonClass: effectiveTier === 'ENTERPRISE'
? 'bg-paper text-concrete cursor-default'
: 'bg-ink text-white shadow-xl shadow-ink/20 hover:scale-[1.02] active:scale-95',
onClick: () => { window.location.href = 'mailto:sales@momento.app'; },
onClick: () => { window.location.href = 'mailto:sales@memento-note.com'; },
},
];

View File

@@ -298,21 +298,21 @@ function SidebarCarnetItem({
<button
onClick={(e) => { e.stopPropagation(); onAddSubNotebook() }}
className="p-1 hover:bg-ink/10 rounded-md transition-all text-concrete hover:text-ink"
title={t('notebook.createSubNotebook') || 'Add sub-carnet'}
title={t('notebook.createSubNotebook')}
>
<Plus size={10} />
</button>
<button
onClick={(e) => { e.stopPropagation(); onRename() }}
className="p-1 hover:bg-ink/10 rounded-md transition-all text-concrete hover:text-ink"
title={t('notebook.rename') || 'Rename'}
title={t('notebook.rename')}
>
<Pencil size={10} />
</button>
<button
onClick={(e) => { e.stopPropagation(); onDelete() }}
className="p-1 hover:bg-rose-50 rounded-md transition-all text-concrete hover:text-rose-500"
title={t('notebook.delete') || 'Delete'}
title={t('notebook.delete')}
>
<Trash2 size={10} />
</button>
@@ -399,7 +399,7 @@ function SidebarCarnetItem({
))}
{isActive && notes.length === 0 && !hasChildren && (
<p className="ps-8 py-2 text-[10px] italic text-muted-foreground/40 font-light">
{t('common.noResults') || 'No notes found'}
{t('common.noResults')}
</p>
)}
</div>
@@ -583,7 +583,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
try {
await moveNotebookToParent(dragId, targetId)
} catch (err) {
toast.error(err instanceof Error ? err.message : t('sidebar.moveFailed') || 'Failed to move notebook')
toast.error(err instanceof Error ? err.message : t('sidebar.moveFailed'))
setOrderedNotebooks(sortedNotebooks)
}
}
@@ -602,7 +602,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
try {
await moveNotebookToParent(dragId, null)
} catch (err) {
toast.error(err instanceof Error ? err.message : t('sidebar.moveFailed') || 'Failed to move notebook')
toast.error(err instanceof Error ? err.message : t('sidebar.moveFailed'))
setOrderedNotebooks(sortedNotebooks)
}
}
@@ -835,14 +835,14 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<DropdownMenuItem asChild>
<Link href="/settings/profile" className="flex items-center gap-2 cursor-pointer">
<User className="h-4 w-4" />
{t('sidebar.profile') || 'Profil'}
{t('sidebar.profile')}
</Link>
</DropdownMenuItem>
{(user as { role?: string } | undefined)?.role === 'ADMIN' && (
<DropdownMenuItem asChild>
<a href="/admin" className="flex items-center gap-2 cursor-pointer">
<Shield className="h-4 w-4" />
{t('nav.adminDashboard') || 'Administration'}
{t('nav.adminDashboard')}
</a>
</DropdownMenuItem>
)}
@@ -852,7 +852,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
onClick={() => signOut({ callbackUrl: '/login' })}
>
<LogOut className="h-4 w-4 me-2" />
{t('sidebar.signOut') || 'Se déconnecter'}
{t('sidebar.signOut')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -873,7 +873,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<button
onClick={() => { router.push('/home') }}
className="p-1.5 text-muted-ink hover:text-ink transition-all hover:bg-white/50 dark:hover:bg-white/10 rounded-lg border border-transparent hover:border-border"
title={t('nav.home') || 'Accueil'}
title={t('nav.home')}
>
<Home size={14} />
</button>
@@ -1016,7 +1016,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<div
className="h-10 rounded-lg border-2 border-dashed border-brand-accent/20 flex items-center justify-center text-[11px] text-brand-accent/50"
>
{t('sidebar.dropToRoot') || 'Drop here to move to root'}
{t('sidebar.dropToRoot')}
</div>
)}
</div>
@@ -1034,7 +1034,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
</p>
<div className="px-4 py-8 text-center border border-dashed border-border rounded-2xl bg-paper/30">
<Clock size={24} className="mx-auto text-concrete/40 mb-3" />
<p className="text-[11px] text-concrete italic">{t('sidebar.noReminders') || 'No active reminders.'}</p>
<p className="text-[11px] text-concrete italic">{t('sidebar.noReminders')}</p>
</div>
</motion.div>
) : activeView === 'agents' ? (
@@ -1118,7 +1118,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
)}
>
<Users size={14} className={searchParams.get('shared') === '1' && pathname === '/home' ? 'text-accent' : 'text-muted-ink group-hover:text-ink'} />
<span>{t('sidebar.sharedWithMe') || 'Shared'}</span>
<span>{t('sidebar.sharedWithMe')}</span>
</Link>
<Link
@@ -1175,7 +1175,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-sm font-semibold mb-4 font-memento-serif">
{t('notebook.rename') || 'Rename notebook'}
{t('notebook.rename')}
</h3>
<input
autoFocus
@@ -1183,7 +1183,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
onChange={(e) => setRenameValue(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleConfirmRename(); if (e.key === 'Escape') setRenamingNotebook(null) }}
className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-transparent focus:outline-none focus:ring-2 focus:ring-foreground/20"
placeholder={t('notebook.namePlaceholder') || 'Notebook name'}
placeholder={t('notebook.namePlaceholder')}
/>
<div className="flex justify-end gap-2 mt-4">
<button
@@ -1191,14 +1191,14 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
disabled={isRenaming}
className="px-4 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors rounded-lg"
>
{t('common.cancel') || 'Cancel'}
{t('common.cancel')}
</button>
<button
onClick={handleConfirmRename}
disabled={isRenaming || !renameValue.trim()}
className="px-4 py-1.5 text-xs font-medium bg-foreground text-background rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50"
>
{isRenaming ? '...' : (t('common.confirm') || 'Rename')}
{isRenaming ? '...' : t('notebook.confirmRename')}
</button>
</div>
</motion.div>
@@ -1228,7 +1228,7 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
<Trash2 size={16} className="text-red-500" />
</div>
<h3 className="text-sm font-semibold mb-1 font-memento-serif">
{t('notebook.trashTitle') || 'Move to trash'}
{t('notebook.trashTitle')}
</h3>
<p className="text-xs text-muted-foreground mb-4">
{t('notebook.trashConfirm', { name: deletingNotebook.name }) || `Move "${deletingNotebook.name}" to trash? You can restore it within 30 days.`}
@@ -1244,14 +1244,14 @@ export function Sidebar({ className, user }: { className?: string; user?: any })
disabled={isDeleting}
className="px-4 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors rounded-lg"
>
{t('common.cancel') || 'Cancel'}
{t('common.cancel')}
</button>
<button
onClick={handleConfirmDelete}
disabled={isDeleting}
className="px-4 py-1.5 text-xs font-medium bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors disabled:opacity-50"
>
{isDeleting ? '...' : (t('notebook.moveToTrash') || 'Move to trash')}
{isDeleting ? '...' : t('notebook.moveToTrash')}
</button>
</div>
</motion.div>

View File

@@ -188,7 +188,7 @@ export function UsageMeter({ className }: UsageMeterProps) {
onClick={(e) => { e.stopPropagation(); router.push('/settings/billing'); }}
className="w-full mt-2 py-2 bg-brand-accent/10 hover:bg-brand-accent text-brand-accent hover:text-white text-[9px] font-bold uppercase tracking-widest rounded-xl transition-all duration-300 border border-brand-accent/20"
>
{t('usageMeter.upgradePricing') || 'Passer à Pro'}
{t('usageMeter.upgradePricing')}
</button>
)}
</div>