epic-ux-design #1
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Plus, X, Folder, Briefcase, FileText, Zap, BarChart3, Globe, Sparkles, Book, Heart, Crown, Music, Building2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
@@ -47,7 +46,6 @@ interface CreateNotebookDialogProps {
|
||||
}
|
||||
|
||||
export function CreateNotebookDialog({ open, onOpenChange }: CreateNotebookDialogProps) {
|
||||
const router = useRouter()
|
||||
const { t } = useLanguage()
|
||||
const { createNotebookOptimistic } = useNotebooks()
|
||||
const [name, setName] = useState('')
|
||||
@@ -68,9 +66,8 @@ export function CreateNotebookDialog({ open, onOpenChange }: CreateNotebookDialo
|
||||
icon: selectedIcon,
|
||||
color: selectedColor,
|
||||
})
|
||||
// Close dialog — no full page reload needed
|
||||
// Close dialog — context already updated sidebar state
|
||||
onOpenChange?.(false)
|
||||
router.refresh()
|
||||
} catch (error) {
|
||||
console.error('Failed to create notebook:', error)
|
||||
} finally {
|
||||
|
||||
@@ -26,7 +26,6 @@ export function DeleteNotebookDialog({ notebook, open, onOpenChange }: DeleteNot
|
||||
try {
|
||||
await deleteNotebook(notebook.id)
|
||||
onOpenChange(false)
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
// Error already handled in UI
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
@@ -14,41 +13,36 @@ import {
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useNotebooks } from '@/context/notebooks-context'
|
||||
import { Notebook } from '@/lib/types'
|
||||
|
||||
interface EditNotebookDialogProps {
|
||||
notebook: any
|
||||
notebook: Notebook
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function EditNotebookDialog({ notebook, open, onOpenChange }: EditNotebookDialogProps) {
|
||||
const router = useRouter()
|
||||
const { updateNotebook } = useNotebooks()
|
||||
const { t } = useLanguage()
|
||||
const [name, setName] = useState(notebook?.name || '')
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setName(notebook?.name || '')
|
||||
}
|
||||
}, [open, notebook?.name])
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!name.trim()) return
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/notebooks/${notebook.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: name.trim() }),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
onOpenChange(false)
|
||||
window.location.reload()
|
||||
} else {
|
||||
const error = await response.json()
|
||||
}
|
||||
} catch (error) {
|
||||
// Error already handled in UI
|
||||
await updateNotebook(notebook.id, { name: name.trim() })
|
||||
onOpenChange(false)
|
||||
} catch {
|
||||
// Error handled in UI
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
@@ -63,7 +57,6 @@ export function EditNotebookDialog({ notebook, open, onOpenChange }: EditNoteboo
|
||||
{t('notebook.editDescription')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
@@ -80,7 +73,6 @@ export function EditNotebookDialog({ notebook, open, onOpenChange }: EditNoteboo
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -9,47 +9,46 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Notebook } from '@/lib/types'
|
||||
|
||||
interface NotebookActionsProps {
|
||||
notebook: any
|
||||
notebook: Notebook
|
||||
onEdit: () => void
|
||||
onDelete: () => void
|
||||
onSummary?: () => void // NEW: Summary action callback (IA6)
|
||||
onSummary?: () => void
|
||||
}
|
||||
|
||||
export function NotebookActions({ notebook, onEdit, onDelete, onSummary }: NotebookActionsProps) {
|
||||
const { t } = useLanguage()
|
||||
return (
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex items-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<MoreVertical className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{onSummary && (
|
||||
<DropdownMenuItem onClick={onSummary}>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
{t('notebook.summary')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={onEdit}>
|
||||
<Edit2 className="h-4 w-4 mr-2" />
|
||||
{t('notebook.edit')}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<MoreVertical className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{onSummary && (
|
||||
<DropdownMenuItem onClick={onSummary}>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
{t('notebook.summary')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={onDelete}
|
||||
className="text-red-600"
|
||||
>
|
||||
{t('notebook.delete')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
<DropdownMenuItem onClick={onEdit}>
|
||||
<Edit2 className="h-4 w-4 mr-2" />
|
||||
{t('notebook.edit')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={onDelete}
|
||||
className="text-red-600"
|
||||
>
|
||||
{t('notebook.delete')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { EditNotebookDialog } from './edit-notebook-dialog'
|
||||
import { NotebookSummaryDialog } from './notebook-summary-dialog'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
import { useLabels } from '@/context/LabelContext'
|
||||
import { Notebook } from '@/lib/types'
|
||||
|
||||
// Map icon names to lucide-react components
|
||||
const ICON_MAP: Record<string, LucideIcon> = {
|
||||
@@ -35,8 +36,7 @@ const ICON_MAP: Record<string, LucideIcon> = {
|
||||
|
||||
// Function to get icon component by name
|
||||
const getNotebookIcon = (iconName: string) => {
|
||||
const IconComponent = ICON_MAP[iconName] || Folder
|
||||
return IconComponent
|
||||
return ICON_MAP[iconName] || Folder
|
||||
}
|
||||
|
||||
export function NotebooksList() {
|
||||
@@ -49,9 +49,9 @@ export function NotebooksList() {
|
||||
const { labels } = useLabels()
|
||||
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
|
||||
const [editingNotebook, setEditingNotebook] = useState<any>(null)
|
||||
const [deletingNotebook, setDeletingNotebook] = useState<any>(null)
|
||||
const [summaryNotebook, setSummaryNotebook] = useState<any>(null)
|
||||
const [editingNotebook, setEditingNotebook] = useState<Notebook | null>(null)
|
||||
const [deletingNotebook, setDeletingNotebook] = useState<Notebook | null>(null)
|
||||
const [summaryNotebook, setSummaryNotebook] = useState<Notebook | null>(null)
|
||||
const [expandedNotebook, setExpandedNotebook] = useState<string | null>(null)
|
||||
|
||||
const currentNotebookId = searchParams.get('notebook')
|
||||
@@ -140,7 +140,7 @@ export function NotebooksList() {
|
||||
</div>
|
||||
|
||||
{/* Notebooks Loop */}
|
||||
{notebooks.map((notebook: any) => {
|
||||
{notebooks.map((notebook: Notebook) => {
|
||||
const isActive = currentNotebookId === notebook.id
|
||||
const isExpanded = expandedNotebook === notebook.id
|
||||
const isDragOver = dragOverNotebookId === notebook.id
|
||||
@@ -151,39 +151,48 @@ export function NotebooksList() {
|
||||
return (
|
||||
<div key={notebook.id} className="group flex flex-col">
|
||||
{isActive ? (
|
||||
// Active notebook with expanded labels - STYLE MATCH Sidebar
|
||||
// Active notebook with expanded labels
|
||||
<div
|
||||
onDrop={(e) => handleDrop(e, notebook.id)}
|
||||
onDragOver={(e) => handleDragOver(e, notebook.id)}
|
||||
onDragLeave={handleDragLeave}
|
||||
className={cn(
|
||||
"flex flex-col mr-2 rounded-r-full overflow-hidden transition-all",
|
||||
"flex flex-col mr-2 rounded-r-full overflow-hidden transition-all relative",
|
||||
!notebook.color && "bg-primary/10 dark:bg-primary/20",
|
||||
isDragOver && "ring-2 ring-primary ring-dashed"
|
||||
)}
|
||||
style={notebook.color ? { backgroundColor: `${notebook.color}20` } : undefined}
|
||||
>
|
||||
{/* Header - allow pointer events for expand button */}
|
||||
{/* Header */}
|
||||
<div className="pointer-events-auto flex items-center justify-between px-6 py-3">
|
||||
<div className="flex items-center gap-4 min-w-0">
|
||||
<div className="flex items-center gap-4 min-w-0 flex-1">
|
||||
<NotebookIcon
|
||||
className={cn("w-5 h-5 flex-shrink-0 fill-current", !notebook.color && "text-primary dark:text-primary-foreground")}
|
||||
style={notebook.color ? { color: notebook.color } : undefined}
|
||||
/>
|
||||
<span
|
||||
className={cn("text-sm font-medium tracking-wide truncate max-w-[120px]", !notebook.color && "text-primary dark:text-primary-foreground")}
|
||||
className={cn("text-sm font-medium tracking-wide truncate min-w-0", !notebook.color && "text-primary dark:text-primary-foreground")}
|
||||
style={notebook.color ? { color: notebook.color } : undefined}
|
||||
>
|
||||
{notebook.name}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleToggleExpand(notebook.id)}
|
||||
className={cn("transition-colors p-1 flex-shrink-0", !notebook.color && "text-primary hover:text-primary/80 dark:text-primary-foreground dark:hover:text-primary-foreground/80")}
|
||||
style={notebook.color ? { color: notebook.color } : undefined}
|
||||
>
|
||||
<ChevronDown className={cn("w-4 h-4 transition-transform", isExpanded && "rotate-180")} />
|
||||
</button>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{/* Actions menu for active notebook */}
|
||||
<NotebookActions
|
||||
notebook={notebook}
|
||||
onEdit={() => setEditingNotebook(notebook)}
|
||||
onDelete={() => setDeletingNotebook(notebook)}
|
||||
onSummary={() => setSummaryNotebook(notebook)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleToggleExpand(notebook.id)}
|
||||
className={cn("transition-colors p-1", !notebook.color && "text-primary hover:text-primary/80 dark:text-primary-foreground dark:hover:text-primary-foreground/80")}
|
||||
style={notebook.color ? { color: notebook.color } : undefined}
|
||||
>
|
||||
<ChevronDown className={cn("w-4 h-4 transition-transform", isExpanded && "rotate-180")} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contextual Labels Tree */}
|
||||
@@ -221,45 +230,41 @@ export function NotebooksList() {
|
||||
onDragOver={(e) => handleDragOver(e, notebook.id)}
|
||||
onDragLeave={handleDragLeave}
|
||||
className={cn(
|
||||
"flex items-center group relative",
|
||||
"flex items-center relative",
|
||||
isDragOver && "ring-2 ring-blue-500 ring-dashed rounded-r-full mr-2"
|
||||
)}
|
||||
>
|
||||
<div className="w-full flex">
|
||||
<button
|
||||
onClick={() => handleSelectNotebook(notebook.id)}
|
||||
className={cn(
|
||||
"pointer-events-auto flex items-center gap-4 px-6 py-3 rounded-r-full mr-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800/50 transition-colors w-full pr-20",
|
||||
isDragOver && "opacity-50"
|
||||
)}
|
||||
>
|
||||
<NotebookIcon className="w-5 h-5 flex-shrink-0" />
|
||||
<span className="text-sm font-medium tracking-wide truncate min-w-0 text-left">{notebook.name}</span>
|
||||
{notebook.notesCount > 0 && (
|
||||
<span className="text-xs text-gray-400 ml-2 flex-shrink-0">({notebook.notesCount})</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSelectNotebook(notebook.id)}
|
||||
className={cn(
|
||||
"pointer-events-auto flex items-center gap-4 px-6 py-3 rounded-r-full mr-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800/50 transition-colors w-full pr-24",
|
||||
isDragOver && "opacity-50"
|
||||
)}
|
||||
>
|
||||
<NotebookIcon className="w-5 h-5 flex-shrink-0" />
|
||||
<span className="text-sm font-medium tracking-wide truncate min-w-0 text-left">{notebook.name}</span>
|
||||
{(notebook as any).notesCount > 0 && (
|
||||
<span className="text-xs text-gray-400 ml-2 flex-shrink-0">({(notebook as any).notesCount})</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Expand button separate from main click */}
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleToggleExpand(notebook.id); }}
|
||||
className={cn(
|
||||
"absolute right-4 top-1/2 -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 opacity-0 group-hover:opacity-100",
|
||||
expandedNotebook === notebook.id && "opacity-100 rotate-180"
|
||||
)}
|
||||
>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Actions (visible on hover) */}
|
||||
<div className="absolute right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10" style={{ right: '40px' }}>
|
||||
{/* Actions + expand on the right — always rendered, visible on hover */}
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-10">
|
||||
<NotebookActions
|
||||
notebook={notebook}
|
||||
onEdit={() => setEditingNotebook(notebook)}
|
||||
onDelete={() => setDeletingNotebook(notebook)}
|
||||
onSummary={() => setSummaryNotebook(notebook)}
|
||||
/>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleToggleExpand(notebook.id); }}
|
||||
className={cn(
|
||||
"p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors rounded-full hover:bg-gray-200 dark:hover:bg-gray-700",
|
||||
expandedNotebook === notebook.id && "rotate-180"
|
||||
)}
|
||||
>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -302,7 +307,7 @@ export function NotebooksList() {
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setSummaryNotebook(null)
|
||||
}}
|
||||
notebookId={summaryNotebook?.id}
|
||||
notebookId={summaryNotebook?.id ?? null}
|
||||
notebookName={summaryNotebook?.name}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -14,7 +14,7 @@ export function NoteRefreshProvider({ children }: { children: React.ReactNode })
|
||||
|
||||
const triggerRefresh = useCallback(() => {
|
||||
setRefreshKey(prev => prev + 1)
|
||||
}, [refreshKey])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<NoteRefreshContext.Provider value={{ refreshKey, triggerRefresh }}>
|
||||
|
||||
@@ -40,7 +40,7 @@ export interface NotebooksContextValue {
|
||||
error: string | null
|
||||
|
||||
// Actions: Notebooks
|
||||
createNotebookOptimistic: (data: CreateNotebookInput) => Promise<Notebook>
|
||||
createNotebookOptimistic: (data: CreateNotebookInput) => Promise<void>
|
||||
updateNotebook: (notebookId: string, data: UpdateNotebookInput) => Promise<void>
|
||||
deleteNotebook: (notebookId: string) => Promise<void>
|
||||
updateNotebookOrderOptimistic: (notebookIds: string[]) => Promise<void>
|
||||
@@ -114,7 +114,6 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
|
||||
// ===== ACTIONS: NOTEBOOKS =====
|
||||
const createNotebookOptimistic = useCallback(async (data: CreateNotebookInput) => {
|
||||
// Server action sera implémenté plus tard
|
||||
const response = await fetch('/api/notebooks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -125,9 +124,10 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
throw new Error('Failed to create notebook')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result
|
||||
}, [])
|
||||
// Reload notebooks from server to update sidebar state
|
||||
await loadNotebooks()
|
||||
triggerRefresh()
|
||||
}, [loadNotebooks, triggerRefresh])
|
||||
|
||||
const updateNotebook = useCallback(async (notebookId: string, data: UpdateNotebookInput) => {
|
||||
const response = await fetch(`/api/notebooks/${notebookId}`, {
|
||||
|
||||
Reference in New Issue
Block a user