epic-ux-design #1

Open
sepehr wants to merge 21 commits from epic-ux-design into main
7 changed files with 106 additions and 114 deletions
Showing only changes of commit 806f4c4eeb - Show all commits

View File

@@ -1,7 +1,6 @@
'use client' 'use client'
import { useState } from 'react' 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 { Plus, X, Folder, Briefcase, FileText, Zap, BarChart3, Globe, Sparkles, Book, Heart, Crown, Music, Building2 } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
@@ -47,7 +46,6 @@ interface CreateNotebookDialogProps {
} }
export function CreateNotebookDialog({ open, onOpenChange }: CreateNotebookDialogProps) { export function CreateNotebookDialog({ open, onOpenChange }: CreateNotebookDialogProps) {
const router = useRouter()
const { t } = useLanguage() const { t } = useLanguage()
const { createNotebookOptimistic } = useNotebooks() const { createNotebookOptimistic } = useNotebooks()
const [name, setName] = useState('') const [name, setName] = useState('')
@@ -68,9 +66,8 @@ export function CreateNotebookDialog({ open, onOpenChange }: CreateNotebookDialo
icon: selectedIcon, icon: selectedIcon,
color: selectedColor, color: selectedColor,
}) })
// Close dialog — no full page reload needed // Close dialog — context already updated sidebar state
onOpenChange?.(false) onOpenChange?.(false)
router.refresh()
} catch (error) { } catch (error) {
console.error('Failed to create notebook:', error) console.error('Failed to create notebook:', error)
} finally { } finally {

View File

@@ -26,7 +26,6 @@ export function DeleteNotebookDialog({ notebook, open, onOpenChange }: DeleteNot
try { try {
await deleteNotebook(notebook.id) await deleteNotebook(notebook.id)
onOpenChange(false) onOpenChange(false)
window.location.reload()
} catch (error) { } catch (error) {
// Error already handled in UI // Error already handled in UI
} }

View File

@@ -1,7 +1,6 @@
'use client' 'use client'
import { useState } from 'react' import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useLanguage } from '@/lib/i18n' import { useLanguage } from '@/lib/i18n'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
@@ -14,41 +13,36 @@ import {
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { useNotebooks } from '@/context/notebooks-context'
import { Notebook } from '@/lib/types'
interface EditNotebookDialogProps { interface EditNotebookDialogProps {
notebook: any notebook: Notebook
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
} }
export function EditNotebookDialog({ notebook, open, onOpenChange }: EditNotebookDialogProps) { export function EditNotebookDialog({ notebook, open, onOpenChange }: EditNotebookDialogProps) {
const router = useRouter() const { updateNotebook } = useNotebooks()
const { t } = useLanguage() const { t } = useLanguage()
const [name, setName] = useState(notebook?.name || '') const [name, setName] = useState(notebook?.name || '')
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
useEffect(() => {
if (open) {
setName(notebook?.name || '')
}
}, [open, notebook?.name])
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (!name.trim()) return if (!name.trim()) return
setIsSubmitting(true) setIsSubmitting(true)
try { try {
const response = await fetch(`/api/notebooks/${notebook.id}`, { await updateNotebook(notebook.id, { name: name.trim() })
method: 'PATCH', onOpenChange(false)
headers: { 'Content-Type': 'application/json' }, } catch {
body: JSON.stringify({ name: name.trim() }), // Error handled in UI
})
if (response.ok) {
onOpenChange(false)
window.location.reload()
} else {
const error = await response.json()
}
} catch (error) {
// Error already handled in UI
} finally { } finally {
setIsSubmitting(false) setIsSubmitting(false)
} }
@@ -63,7 +57,6 @@ export function EditNotebookDialog({ notebook, open, onOpenChange }: EditNoteboo
{t('notebook.editDescription')} {t('notebook.editDescription')}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
@@ -80,7 +73,6 @@ export function EditNotebookDialog({ notebook, open, onOpenChange }: EditNoteboo
/> />
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button <Button
type="button" type="button"

View File

@@ -9,47 +9,46 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { Notebook } from '@/lib/types'
interface NotebookActionsProps { interface NotebookActionsProps {
notebook: any notebook: Notebook
onEdit: () => void onEdit: () => void
onDelete: () => void onDelete: () => void
onSummary?: () => void // NEW: Summary action callback (IA6) onSummary?: () => void
} }
export function NotebookActions({ notebook, onEdit, onDelete, onSummary }: NotebookActionsProps) { export function NotebookActions({ notebook, onEdit, onDelete, onSummary }: NotebookActionsProps) {
const { t } = useLanguage() const { t } = useLanguage()
return ( return (
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex items-center"> <DropdownMenu>
<DropdownMenu> <DropdownMenuTrigger asChild>
<DropdownMenuTrigger asChild> <Button
<Button variant="ghost"
variant="ghost" size="sm"
size="sm" className="h-8 w-8 p-0"
className="h-8 w-8 p-0" >
> <MoreVertical className="h-3 w-3" />
<MoreVertical className="h-3 w-3" /> </Button>
</Button> </DropdownMenuTrigger>
</DropdownMenuTrigger> <DropdownMenuContent align="end">
<DropdownMenuContent align="end"> {onSummary && (
{onSummary && ( <DropdownMenuItem onClick={onSummary}>
<DropdownMenuItem onClick={onSummary}> <FileText className="h-4 w-4 mr-2" />
<FileText className="h-4 w-4 mr-2" /> {t('notebook.summary')}
{t('notebook.summary')}
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={onEdit}>
<Edit2 className="h-4 w-4 mr-2" />
{t('notebook.edit')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem )}
onClick={onDelete} <DropdownMenuItem onClick={onEdit}>
className="text-red-600" <Edit2 className="h-4 w-4 mr-2" />
> {t('notebook.edit')}
{t('notebook.delete')} </DropdownMenuItem>
</DropdownMenuItem> <DropdownMenuItem
</DropdownMenuContent> onClick={onDelete}
</DropdownMenu> className="text-red-600"
</div> >
{t('notebook.delete')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) )
} }

View File

@@ -15,6 +15,7 @@ import { EditNotebookDialog } from './edit-notebook-dialog'
import { NotebookSummaryDialog } from './notebook-summary-dialog' import { NotebookSummaryDialog } from './notebook-summary-dialog'
import { useLanguage } from '@/lib/i18n' import { useLanguage } from '@/lib/i18n'
import { useLabels } from '@/context/LabelContext' import { useLabels } from '@/context/LabelContext'
import { Notebook } from '@/lib/types'
// Map icon names to lucide-react components // Map icon names to lucide-react components
const ICON_MAP: Record<string, LucideIcon> = { const ICON_MAP: Record<string, LucideIcon> = {
@@ -35,8 +36,7 @@ const ICON_MAP: Record<string, LucideIcon> = {
// Function to get icon component by name // Function to get icon component by name
const getNotebookIcon = (iconName: string) => { const getNotebookIcon = (iconName: string) => {
const IconComponent = ICON_MAP[iconName] || Folder return ICON_MAP[iconName] || Folder
return IconComponent
} }
export function NotebooksList() { export function NotebooksList() {
@@ -49,9 +49,9 @@ export function NotebooksList() {
const { labels } = useLabels() const { labels } = useLabels()
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
const [editingNotebook, setEditingNotebook] = useState<any>(null) const [editingNotebook, setEditingNotebook] = useState<Notebook | null>(null)
const [deletingNotebook, setDeletingNotebook] = useState<any>(null) const [deletingNotebook, setDeletingNotebook] = useState<Notebook | null>(null)
const [summaryNotebook, setSummaryNotebook] = useState<any>(null) const [summaryNotebook, setSummaryNotebook] = useState<Notebook | null>(null)
const [expandedNotebook, setExpandedNotebook] = useState<string | null>(null) const [expandedNotebook, setExpandedNotebook] = useState<string | null>(null)
const currentNotebookId = searchParams.get('notebook') const currentNotebookId = searchParams.get('notebook')
@@ -140,7 +140,7 @@ export function NotebooksList() {
</div> </div>
{/* Notebooks Loop */} {/* Notebooks Loop */}
{notebooks.map((notebook: any) => { {notebooks.map((notebook: Notebook) => {
const isActive = currentNotebookId === notebook.id const isActive = currentNotebookId === notebook.id
const isExpanded = expandedNotebook === notebook.id const isExpanded = expandedNotebook === notebook.id
const isDragOver = dragOverNotebookId === notebook.id const isDragOver = dragOverNotebookId === notebook.id
@@ -151,39 +151,48 @@ export function NotebooksList() {
return ( return (
<div key={notebook.id} className="group flex flex-col"> <div key={notebook.id} className="group flex flex-col">
{isActive ? ( {isActive ? (
// Active notebook with expanded labels - STYLE MATCH Sidebar // Active notebook with expanded labels
<div <div
onDrop={(e) => handleDrop(e, notebook.id)} onDrop={(e) => handleDrop(e, notebook.id)}
onDragOver={(e) => handleDragOver(e, notebook.id)} onDragOver={(e) => handleDragOver(e, notebook.id)}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
className={cn( 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", !notebook.color && "bg-primary/10 dark:bg-primary/20",
isDragOver && "ring-2 ring-primary ring-dashed" isDragOver && "ring-2 ring-primary ring-dashed"
)} )}
style={notebook.color ? { backgroundColor: `${notebook.color}20` } : undefined} 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="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 <NotebookIcon
className={cn("w-5 h-5 flex-shrink-0 fill-current", !notebook.color && "text-primary dark:text-primary-foreground")} 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} style={notebook.color ? { color: notebook.color } : undefined}
/> />
<span <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} style={notebook.color ? { color: notebook.color } : undefined}
> >
{notebook.name} {notebook.name}
</span> </span>
</div> </div>
<button <div className="flex items-center gap-1 flex-shrink-0">
onClick={() => handleToggleExpand(notebook.id)} {/* Actions menu for active notebook */}
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")} <NotebookActions
style={notebook.color ? { color: notebook.color } : undefined} notebook={notebook}
> onEdit={() => setEditingNotebook(notebook)}
<ChevronDown className={cn("w-4 h-4 transition-transform", isExpanded && "rotate-180")} /> onDelete={() => setDeletingNotebook(notebook)}
</button> 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> </div>
{/* Contextual Labels Tree */} {/* Contextual Labels Tree */}
@@ -221,45 +230,41 @@ export function NotebooksList() {
onDragOver={(e) => handleDragOver(e, notebook.id)} onDragOver={(e) => handleDragOver(e, notebook.id)}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
className={cn( className={cn(
"flex items-center group relative", "flex items-center relative",
isDragOver && "ring-2 ring-blue-500 ring-dashed rounded-r-full mr-2" isDragOver && "ring-2 ring-blue-500 ring-dashed rounded-r-full mr-2"
)} )}
> >
<div className="w-full flex"> <button
<button onClick={() => handleSelectNotebook(notebook.id)}
onClick={() => handleSelectNotebook(notebook.id)} className={cn(
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",
"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"
isDragOver && "opacity-50" )}
)} >
> <NotebookIcon className="w-5 h-5 flex-shrink-0" />
<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>
<span className="text-sm font-medium tracking-wide truncate min-w-0 text-left">{notebook.name}</span> {(notebook as any).notesCount > 0 && (
{notebook.notesCount > 0 && ( <span className="text-xs text-gray-400 ml-2 flex-shrink-0">({(notebook as any).notesCount})</span>
<span className="text-xs text-gray-400 ml-2 flex-shrink-0">({notebook.notesCount})</span> )}
)} </button>
</button>
{/* Expand button separate from main click */} {/* Actions + expand on the right — always rendered, visible on hover */}
<button <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">
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' }}>
<NotebookActions <NotebookActions
notebook={notebook} notebook={notebook}
onEdit={() => setEditingNotebook(notebook)} onEdit={() => setEditingNotebook(notebook)}
onDelete={() => setDeletingNotebook(notebook)} onDelete={() => setDeletingNotebook(notebook)}
onSummary={() => setSummaryNotebook(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>
</div> </div>
)} )}
@@ -302,7 +307,7 @@ export function NotebooksList() {
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) setSummaryNotebook(null) if (!open) setSummaryNotebook(null)
}} }}
notebookId={summaryNotebook?.id} notebookId={summaryNotebook?.id ?? null}
notebookName={summaryNotebook?.name} notebookName={summaryNotebook?.name}
/> />
</> </>

View File

@@ -14,7 +14,7 @@ export function NoteRefreshProvider({ children }: { children: React.ReactNode })
const triggerRefresh = useCallback(() => { const triggerRefresh = useCallback(() => {
setRefreshKey(prev => prev + 1) setRefreshKey(prev => prev + 1)
}, [refreshKey]) }, [])
return ( return (
<NoteRefreshContext.Provider value={{ refreshKey, triggerRefresh }}> <NoteRefreshContext.Provider value={{ refreshKey, triggerRefresh }}>

View File

@@ -40,7 +40,7 @@ export interface NotebooksContextValue {
error: string | null error: string | null
// Actions: Notebooks // Actions: Notebooks
createNotebookOptimistic: (data: CreateNotebookInput) => Promise<Notebook> createNotebookOptimistic: (data: CreateNotebookInput) => Promise<void>
updateNotebook: (notebookId: string, data: UpdateNotebookInput) => Promise<void> updateNotebook: (notebookId: string, data: UpdateNotebookInput) => Promise<void>
deleteNotebook: (notebookId: string) => Promise<void> deleteNotebook: (notebookId: string) => Promise<void>
updateNotebookOrderOptimistic: (notebookIds: string[]) => Promise<void> updateNotebookOrderOptimistic: (notebookIds: string[]) => Promise<void>
@@ -114,7 +114,6 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
// ===== ACTIONS: NOTEBOOKS ===== // ===== ACTIONS: NOTEBOOKS =====
const createNotebookOptimistic = useCallback(async (data: CreateNotebookInput) => { const createNotebookOptimistic = useCallback(async (data: CreateNotebookInput) => {
// Server action sera implémenté plus tard
const response = await fetch('/api/notebooks', { const response = await fetch('/api/notebooks', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -125,9 +124,10 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
throw new Error('Failed to create notebook') throw new Error('Failed to create notebook')
} }
const result = await response.json() // Reload notebooks from server to update sidebar state
return result await loadNotebooks()
}, []) triggerRefresh()
}, [loadNotebooks, triggerRefresh])
const updateNotebook = useCallback(async (notebookId: string, data: UpdateNotebookInput) => { const updateNotebook = useCallback(async (notebookId: string, data: UpdateNotebookInput) => {
const response = await fetch(`/api/notebooks/${notebookId}`, { const response = await fetch(`/api/notebooks/${notebookId}`, {