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.
293 lines
8.7 KiB
TypeScript
293 lines
8.7 KiB
TypeScript
import { Button } from "@/components/ui/button"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import {
|
|
Archive,
|
|
ArchiveRestore,
|
|
Bell,
|
|
MoreVertical,
|
|
Palette,
|
|
Pin,
|
|
Users,
|
|
Maximize2,
|
|
FileText,
|
|
Trash2,
|
|
RotateCcw,
|
|
History,
|
|
AlignLeft,
|
|
FileCode2,
|
|
PenLine,
|
|
ListChecks,
|
|
} from "lucide-react"
|
|
import { cn } from "@/lib/utils"
|
|
import { NOTE_COLORS } from "@/lib/types"
|
|
import { useLanguage } from "@/lib/i18n"
|
|
import { ReminderDialog } from "@/components/reminder-dialog"
|
|
import { useState } from "react"
|
|
|
|
interface NoteActionsProps {
|
|
isPinned: boolean
|
|
isArchived: boolean
|
|
currentColor: string
|
|
currentSize?: 'small' | 'medium' | 'large'
|
|
onTogglePin: () => void
|
|
onToggleArchive: () => void
|
|
onColorChange: (color: string) => void
|
|
onSizeChange?: (size: 'small' | 'medium' | 'large') => void
|
|
onDelete: () => void
|
|
onShareCollaborators?: () => void
|
|
isMarkdown?: boolean
|
|
noteType?: string
|
|
onToggleMarkdown?: () => void
|
|
isTrashView?: boolean
|
|
onRestore?: () => void
|
|
onPermanentDelete?: () => void
|
|
onOpenHistory?: () => void
|
|
historyEnabled?: boolean
|
|
noteId?: string
|
|
currentReminder?: Date | null
|
|
onUpdateReminder?: (noteId: string, reminder: Date | null) => void
|
|
className?: string
|
|
}
|
|
|
|
export function NoteActions({
|
|
isPinned,
|
|
isArchived,
|
|
currentColor,
|
|
currentSize = 'small',
|
|
onTogglePin,
|
|
onToggleArchive,
|
|
onColorChange,
|
|
onSizeChange,
|
|
onDelete,
|
|
onShareCollaborators,
|
|
isMarkdown = false,
|
|
noteType = 'text',
|
|
onToggleMarkdown,
|
|
isTrashView,
|
|
onRestore,
|
|
onPermanentDelete,
|
|
onOpenHistory,
|
|
historyEnabled = false,
|
|
noteId,
|
|
currentReminder,
|
|
onUpdateReminder,
|
|
className
|
|
}: NoteActionsProps) {
|
|
const { t } = useLanguage()
|
|
const [showReminder, setShowReminder] = useState(false)
|
|
|
|
// Trash view: show only Restore and Permanent Delete
|
|
if (isTrashView) {
|
|
return (
|
|
<div
|
|
className={cn("flex items-center justify-end gap-1", className)}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Restore Button */}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 gap-1 px-2 text-xs"
|
|
onClick={onRestore}
|
|
title={t('trash.restore')}
|
|
>
|
|
<RotateCcw className="h-4 w-4" />
|
|
<span className="hidden sm:inline">{t('trash.restore')}</span>
|
|
</Button>
|
|
|
|
{/* Permanent Delete Button */}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 gap-1 px-2 text-xs text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
|
|
onClick={onPermanentDelete}
|
|
title={t('trash.permanentDelete')}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
<span className="hidden sm:inline">{t('trash.permanentDelete')}</span>
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={cn("flex items-center justify-end gap-1", className)}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Reminder */}
|
|
{noteId && onUpdateReminder && (
|
|
<>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className={cn("h-8 w-8 p-0", currentReminder && "text-primary")}
|
|
title={t('reminder.setReminder')}
|
|
onClick={() => setShowReminder(true)}
|
|
>
|
|
<Bell className="h-4 w-4" />
|
|
</Button>
|
|
<div onClick={(e) => e.stopPropagation()}>
|
|
<ReminderDialog
|
|
open={showReminder}
|
|
onOpenChange={setShowReminder}
|
|
currentReminder={currentReminder || null}
|
|
onSave={(date) => {
|
|
onUpdateReminder(noteId, date)
|
|
setShowReminder(false)
|
|
}}
|
|
onRemove={() => {
|
|
onUpdateReminder(noteId, null)
|
|
setShowReminder(false)
|
|
}}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Color Palette */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" title={t('notes.changeColor')}>
|
|
<Palette className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<div className="grid grid-cols-5 gap-2 p-2">
|
|
{Object.entries(NOTE_COLORS).map(([colorName, classes]) => (
|
|
<button
|
|
key={colorName}
|
|
className={cn(
|
|
'h-8 w-8 rounded-full border-2 transition-transform hover:scale-110',
|
|
classes.bg,
|
|
currentColor === colorName ? 'border-gray-900 dark:border-gray-100' : 'border-gray-300 dark:border-gray-700'
|
|
)}
|
|
onClick={() => onColorChange(colorName)}
|
|
title={colorName}
|
|
/>
|
|
))}
|
|
</div>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{onToggleMarkdown && (() => {
|
|
const iconMap: Record<string, React.ElementType> = { text: AlignLeft, markdown: FileCode2, richtext: PenLine, checklist: ListChecks }
|
|
const TypeIcon = iconMap[noteType] || FileText
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className={cn("h-8 gap-1 px-2 text-xs", noteType !== 'text' && "text-primary bg-primary/10")}
|
|
title={noteType === 'markdown' ? 'Markdown' : noteType === 'richtext' ? 'Rich Text' : noteType === 'checklist' ? 'Checklist' : 'Text'}
|
|
onClick={onToggleMarkdown}
|
|
>
|
|
<TypeIcon className="h-4 w-4" />
|
|
<span className="hidden sm:inline">{noteType === 'markdown' ? 'MD' : noteType === 'richtext' ? 'RT' : noteType === 'checklist' ? 'CL' : 'TXT'}</span>
|
|
</Button>
|
|
)
|
|
})()}
|
|
|
|
{/* More Options */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" aria-label={t('notes.moreOptions')}>
|
|
<MoreVertical className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
{/* Pin/Unpin Option */}
|
|
<DropdownMenuItem onClick={onTogglePin}>
|
|
{isPinned ? (
|
|
<>
|
|
<Pin className="h-4 w-4 mr-2" />
|
|
{t('notes.unpin')}
|
|
</>
|
|
) : (
|
|
<>
|
|
<Pin className="h-4 w-4 mr-2" />
|
|
{t('notes.pin')}
|
|
</>
|
|
)}
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem onClick={onToggleArchive}>
|
|
{isArchived ? (
|
|
<>
|
|
<ArchiveRestore className="h-4 w-4 mr-2" />
|
|
{t('notes.unarchive')}
|
|
</>
|
|
) : (
|
|
<>
|
|
<Archive className="h-4 w-4 mr-2" />
|
|
{t('notes.archive')}
|
|
</>
|
|
)}
|
|
</DropdownMenuItem>
|
|
|
|
{onOpenHistory && (
|
|
<DropdownMenuItem onClick={onOpenHistory}>
|
|
<History className="h-4 w-4 mr-2" />
|
|
{historyEnabled
|
|
? t('notes.history')
|
|
: (t('notes.enableHistory') || "Activer l'historique")}
|
|
</DropdownMenuItem>
|
|
)}
|
|
|
|
{/* Size Selector */}
|
|
{onSizeChange && (
|
|
<>
|
|
<DropdownMenuSeparator />
|
|
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
|
{t('notes.size')}
|
|
</div>
|
|
{(['small', 'medium', 'large'] as const).map((size) => (
|
|
<DropdownMenuItem
|
|
key={size}
|
|
onSelect={(e) => {
|
|
onSizeChange?.(size);
|
|
}}
|
|
className={cn(
|
|
"capitalize",
|
|
currentSize === size && "bg-accent"
|
|
)}
|
|
>
|
|
<Maximize2 className="h-4 w-4 mr-2" />
|
|
{t(`notes.${size}` as const)}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</>
|
|
)}
|
|
|
|
{/* Collaborators */}
|
|
{onShareCollaborators && (
|
|
<>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onShareCollaborators()
|
|
}}
|
|
>
|
|
<Users className="h-4 w-4 mr-2" />
|
|
{t('notes.shareWithCollaborators')}
|
|
</DropdownMenuItem>
|
|
</>
|
|
)}
|
|
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={onDelete} className="text-red-600 dark:text-red-400">
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
{t('notes.delete')}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
)
|
|
}
|