All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m9s
- Schema: soft delete with trashedAt on Notebook model - API: PATCH/GET notebooks support trashedAt filtering with cascade - Sidebar: recursive tree rendering with collapse/expand, visual guides, hover actions - HierarchicalNotebookSelector: portal-based dropdown with search, breadcrumbs, dropUp support - AI chat: context selector with Toutes mes notes + notebook selector - Agent detail: flat selects replaced with HierarchicalNotebookSelector - Breadcrumb: notebook path display on home page - Trash view: card grid with countdown, restore/permanent delete - CSS: design tokens (ink, paper, blueprint, concrete, etc.) - Types: parentId, trashedAt added to Notebook interface
148 lines
5.9 KiB
TypeScript
148 lines
5.9 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { motion, AnimatePresence } from 'motion/react'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
import { useNotebooks } from '@/context/notebooks-context'
|
|
|
|
interface CreateNotebookDialogProps {
|
|
open?: boolean
|
|
onOpenChange?: (open: boolean) => void
|
|
parentNotebookId?: string | null
|
|
}
|
|
|
|
export function CreateNotebookDialog({ open, onOpenChange, parentNotebookId }: CreateNotebookDialogProps) {
|
|
const { t } = useLanguage()
|
|
const { createNotebookOptimistic, notebooks } = useNotebooks()
|
|
const [name, setName] = useState('')
|
|
const [selectedParentId, setSelectedParentId] = useState<string | null>(null)
|
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
|
|
const rootNotebooks = notebooks.filter(nb => !nb.parentId && !nb.trashedAt)
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setSelectedParentId(parentNotebookId ?? null)
|
|
setName('')
|
|
}
|
|
}, [open, parentNotebookId])
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!name.trim()) return
|
|
setIsSubmitting(true)
|
|
try {
|
|
await createNotebookOptimistic({
|
|
name: name.trim(),
|
|
icon: 'folder',
|
|
color: '#64748B',
|
|
parentId: selectedParentId,
|
|
})
|
|
setName('')
|
|
onOpenChange?.(false)
|
|
} catch (err) {
|
|
console.error('Failed to create notebook:', err)
|
|
} finally {
|
|
setIsSubmitting(false)
|
|
}
|
|
}
|
|
|
|
const handleClose = () => {
|
|
setName('')
|
|
setSelectedParentId(null)
|
|
onOpenChange?.(false)
|
|
}
|
|
|
|
const isSubNotebook = !!parentNotebookId
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{open && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
onClick={handleClose}
|
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
|
/>
|
|
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.92, y: 20 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.92, y: 20 }}
|
|
transition={{ type: 'spring', damping: 28, stiffness: 280 }}
|
|
className="relative w-full max-w-md bg-[#F2F0E9] dark:bg-zinc-900 border border-black/10 dark:border-white/10 shadow-2xl rounded-2xl p-8"
|
|
>
|
|
<h3 className="text-2xl font-memento-serif font-medium text-foreground mb-2">
|
|
{isSubNotebook
|
|
? (t('notebook.createSubNotebook') || 'Nouveau sous-carnet')
|
|
: (t('notebook.createNew') || 'Nouveau carnet')
|
|
}
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mb-6 font-light">
|
|
{isSubNotebook && parentNotebookId
|
|
? `${t('notebook.under') || 'Sous'} ${notebooks.find(nb => nb.id === parentNotebookId)?.name || ''}`
|
|
: (t('notebook.createDescription') || 'Donnez un nom à votre nouveau carnet.')
|
|
}
|
|
</p>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div>
|
|
<label className="block text-[11px] uppercase tracking-widest font-bold text-muted-foreground mb-2">
|
|
{t('notebook.name') || 'Nom du carnet'}
|
|
</label>
|
|
<input
|
|
autoFocus
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder={t('notebook.namePlaceholder') || 'Ex. : Projets, Recherche…'}
|
|
className="w-full bg-white dark:bg-zinc-800 border border-black/12 dark:border-white/15 rounded-lg px-4 py-3 outline-none focus:border-foreground/40 dark:focus:border-white/40 transition-colors font-memento-serif italic text-lg text-foreground placeholder:text-muted-foreground/40"
|
|
/>
|
|
</div>
|
|
|
|
{!isSubNotebook && rootNotebooks.length > 0 && (
|
|
<div>
|
|
<label className="block text-[11px] uppercase tracking-widest font-bold text-muted-foreground mb-2">
|
|
{t('notebook.parentNotebook') || 'Carnet parent'}
|
|
</label>
|
|
<select
|
|
value={selectedParentId || ''}
|
|
onChange={(e) => setSelectedParentId(e.target.value || null)}
|
|
className="w-full bg-white dark:bg-zinc-800 border border-black/12 dark:border-white/15 rounded-lg px-4 py-3 outline-none focus:border-foreground/40 transition-colors text-sm text-foreground"
|
|
>
|
|
<option value="">{t('notebook.noParent') || 'Aucun (racine)'}</option>
|
|
{rootNotebooks.map(nb => (
|
|
<option key={nb.id} value={nb.id}>{nb.name}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-3 pt-2">
|
|
<button
|
|
type="button"
|
|
onClick={handleClose}
|
|
className="flex-1 py-3 border border-black/12 dark:border-white/12 rounded-xl text-sm font-medium text-muted-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
|
|
>
|
|
{t('notebook.cancel') || 'Annuler'}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={!name.trim() || isSubmitting}
|
|
className="flex-1 py-3 bg-foreground text-background rounded-xl text-sm font-medium hover:opacity-90 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed"
|
|
>
|
|
{isSubmitting
|
|
? (t('notebook.creating') || 'Création…')
|
|
: (t('notebook.create') || 'Créer')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</motion.div>
|
|
</div>
|
|
)}
|
|
</AnimatePresence>
|
|
)
|
|
}
|