chore: snapshot before performance optimization
This commit is contained in:
@@ -71,6 +71,7 @@ export function LabelManagementDialog(props: LabelManagementDialogProps = {}) {
|
||||
const dialogContent = (
|
||||
<DialogContent
|
||||
className="max-w-md"
|
||||
dir={language === 'fa' || language === 'ar' ? 'rtl' : 'ltr'}
|
||||
onInteractOutside={(event) => {
|
||||
// Prevent dialog from closing when interacting with Sonner toasts
|
||||
const target = event.target as HTMLElement;
|
||||
@@ -205,14 +206,14 @@ export function LabelManagementDialog(props: LabelManagementDialogProps = {}) {
|
||||
|
||||
if (controlled) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange} dir={language === 'fa' || language === 'ar' ? 'rtl' : 'ltr'}>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
{dialogContent}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog dir={language === 'fa' || language === 'ar' ? 'rtl' : 'ltr'}>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon" title={t('labels.manage')}>
|
||||
<Settings className="h-5 w-5" />
|
||||
|
||||
@@ -10,6 +10,16 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Pin, Bell, GripVertical, X, Link2, FolderOpen, StickyNote, LucideIcon, Folder, Briefcase, FileText, Zap, BarChart3, Globe, Sparkles, Book, Heart, Crown, Music, Building2, LogOut, Trash2 } from 'lucide-react'
|
||||
import { useState, useEffect, useTransition, useOptimistic, memo } from 'react'
|
||||
import { useSession } from 'next-auth/react'
|
||||
@@ -149,6 +159,7 @@ export const NoteCard = memo(function NoteCard({
|
||||
const { notebooks, moveNoteToNotebookOptimistic } = useNotebooks()
|
||||
const [, startTransition] = useTransition()
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [showCollaboratorDialog, setShowCollaboratorDialog] = useState(false)
|
||||
const [collaborators, setCollaborators] = useState<any[]>([])
|
||||
const [owner, setOwner] = useState<any>(null)
|
||||
@@ -232,16 +243,13 @@ export const NoteCard = memo(function NoteCard({
|
||||
}, [note.id, note.userId, isSharedNote, currentUserId, session?.user])
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (confirm(t('notes.confirmDelete'))) {
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
await deleteNote(note.id)
|
||||
// Refresh global labels to reflect garbage collection
|
||||
await refreshLabels()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete note:', error)
|
||||
setIsDeleting(false)
|
||||
}
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
await deleteNote(note.id)
|
||||
await refreshLabels()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete note:', error)
|
||||
setIsDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,14 +304,14 @@ export const NoteCard = memo(function NoteCard({
|
||||
}
|
||||
|
||||
const handleCheckItem = async (checkItemId: string) => {
|
||||
if (note.type === 'checklist' && note.checkItems) {
|
||||
if (note.type === 'checklist' && Array.isArray(note.checkItems)) {
|
||||
const updatedItems = note.checkItems.map(item =>
|
||||
item.id === checkItemId ? { ...item, checked: !item.checked } : item
|
||||
)
|
||||
startTransition(async () => {
|
||||
addOptimisticNote({ checkItems: updatedItems })
|
||||
await updateNote(note.id, { checkItems: updatedItems })
|
||||
router.refresh()
|
||||
// No router.refresh() — optimistic update is sufficient and avoids grid rebuild
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -324,7 +332,7 @@ export const NoteCard = memo(function NoteCard({
|
||||
startTransition(async () => {
|
||||
addOptimisticNote({ autoGenerated: null })
|
||||
await removeFusedBadge(note.id)
|
||||
router.refresh()
|
||||
// No router.refresh() — optimistic update is sufficient and avoids grid rebuild
|
||||
})
|
||||
}
|
||||
|
||||
@@ -525,7 +533,7 @@ export const NoteCard = memo(function NoteCard({
|
||||
<NoteImages images={optimisticNote.images || []} title={optimisticNote.title} />
|
||||
|
||||
{/* Link Previews */}
|
||||
{optimisticNote.links && optimisticNote.links.length > 0 && (
|
||||
{Array.isArray(optimisticNote.links) && optimisticNote.links.length > 0 && (
|
||||
<div className="flex flex-col gap-2 mb-2">
|
||||
{optimisticNote.links.map((link, idx) => (
|
||||
<a
|
||||
@@ -564,7 +572,7 @@ export const NoteCard = memo(function NoteCard({
|
||||
)}
|
||||
|
||||
{/* Labels - using shared LabelBadge component */}
|
||||
{optimisticNote.notebookId && optimisticNote.labels && optimisticNote.labels.length > 0 && (
|
||||
{optimisticNote.notebookId && Array.isArray(optimisticNote.labels) && optimisticNote.labels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-3">
|
||||
{optimisticNote.labels.map((label) => (
|
||||
<LabelBadge key={label} label={label} />
|
||||
@@ -605,7 +613,7 @@ export const NoteCard = memo(function NoteCard({
|
||||
onToggleArchive={handleToggleArchive}
|
||||
onColorChange={handleColorChange}
|
||||
onSizeChange={handleSizeChange}
|
||||
onDelete={handleDelete}
|
||||
onDelete={() => setShowDeleteDialog(true)}
|
||||
onShareCollaborators={() => setShowCollaboratorDialog(true)}
|
||||
className="absolute bottom-0 left-0 right-0 p-2 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
/>
|
||||
@@ -656,6 +664,24 @@ export const NoteCard = memo(function NoteCard({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t('notes.confirmDeleteTitle') || t('notes.delete')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t('notes.confirmDelete') || 'Are you sure you want to delete this note?'}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('common.cancel') || 'Cancel'}</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" onClick={handleDelete}>
|
||||
{t('notes.delete') || 'Delete'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
@@ -1,12 +1,13 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn, asArray } from "@/lib/utils"
|
||||
|
||||
interface NoteImagesProps {
|
||||
images: string[]
|
||||
title?: string | null
|
||||
}
|
||||
|
||||
export function NoteImages({ images, title }: NoteImagesProps) {
|
||||
if (!images || images.length === 0) return null
|
||||
export function NoteImages({ images: rawImages, title }: NoteImagesProps) {
|
||||
const images = asArray<string>(rawImages)
|
||||
if (images.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
|
||||
@@ -771,14 +771,14 @@ export function NoteInlineEditor({
|
||||
)}
|
||||
|
||||
{/* Images */}
|
||||
{note.images && note.images.length > 0 && (
|
||||
{Array.isArray(note.images) && note.images.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<EditorImages images={note.images} onRemove={handleRemoveImage} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Link previews */}
|
||||
{note.links && note.links.length > 0 && (
|
||||
{Array.isArray(note.links) && note.links.length > 0 && (
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
{note.links.map((link, idx) => (
|
||||
<div key={idx} className="group relative flex overflow-hidden rounded-xl border border-border/60 bg-background/60">
|
||||
|
||||
@@ -494,7 +494,6 @@ export function NoteInput({
|
||||
reminder: currentReminder,
|
||||
isMarkdown,
|
||||
labels: selectedLabels.length > 0 ? selectedLabels : undefined,
|
||||
sharedWith: collaborators.length > 0 ? collaborators : undefined,
|
||||
notebookId: currentNotebookId, // Assign note to current notebook if in one
|
||||
})
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ function SortableNoteListItem({
|
||||
<Clock className="h-2.5 w-2.5" />
|
||||
{timeAgo}
|
||||
</span>
|
||||
{note.labels && note.labels.length > 0 && (
|
||||
{Array.isArray(note.labels) && note.labels.length > 0 && (
|
||||
<>
|
||||
<span className="text-muted-foreground/30">·</span>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -307,8 +307,8 @@ export function NotesTabsView({ notes, onEdit, currentNotebookId }: NotesTabsVie
|
||||
try {
|
||||
const newNote = await createNote({
|
||||
content: '',
|
||||
title: null,
|
||||
notebookId: currentNotebookId || null,
|
||||
title: undefined,
|
||||
notebookId: currentNotebookId || undefined,
|
||||
skipRevalidation: true
|
||||
})
|
||||
if (!newNote) return
|
||||
|
||||
196
keep-notes/components/ui/alert-dialog.tsx
Normal file
196
keep-notes/components/ui/alert-dialog.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
|
||||
size?: "default" | "sm"
|
||||
}) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[size=default]:sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn(
|
||||
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn(
|
||||
"text-lg font-semibold sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogMedia({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-media"
|
||||
className={cn(
|
||||
"mb-2 inline-flex size-16 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
|
||||
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||
return (
|
||||
<Button variant={variant} size={size} asChild>
|
||||
<AlertDialogPrimitive.Action
|
||||
data-slot="alert-dialog-action"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
|
||||
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||
return (
|
||||
<Button variant={variant} size={size} asChild>
|
||||
<AlertDialogPrimitive.Cancel
|
||||
data-slot="alert-dialog-cancel"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogMedia,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
@@ -22,9 +22,11 @@ const buttonVariants = cva(
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
@@ -46,7 +48,7 @@ function Button({
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
||||
33
keep-notes/components/ui/collapsible.tsx
Normal file
33
keep-notes/components/ui/collapsible.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
Reference in New Issue
Block a user