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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
126
memento-note/components/legal/cookie-preferences-dialog.tsx
Normal file
126
memento-note/components/legal/cookie-preferences-dialog.tsx
Normal 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>
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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'; },
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user