fix: improve note interactions and markdown LaTeX support
## Bug Fixes ### Note Card Actions - Fix broken size change functionality (missing state declaration) - Implement React 19 useOptimistic for instant UI feedback - Add startTransition for non-blocking updates - Ensure smooth animations without page refresh - All note actions now work: pin, archive, color, size, checklist ### Markdown LaTeX Rendering - Add remark-math and rehype-katex plugins - Support inline equations with dollar sign syntax - Support block equations with double dollar sign syntax - Import KaTeX CSS for proper styling - Equations now render correctly instead of showing raw LaTeX ## Technical Details - Replace undefined currentNote references with optimistic state - Add optimistic updates before server actions for instant feedback - Use router.refresh() in transitions for smart cache invalidation - Install remark-math, rehype-katex, and katex packages ## Testing - Build passes successfully with no TypeScript errors - Dev server hot-reloads changes correctly
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef, useCallback, memo } from 'react';
|
||||
import { Note } from '@/lib/types';
|
||||
import { NoteCard } from './note-card';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { NoteEditor } from './note-editor';
|
||||
import { updateFullOrder } from '@/app/actions/notes';
|
||||
import { useResizeObserver } from '@/hooks/use-resize-observer';
|
||||
@@ -13,18 +13,32 @@ interface MasonryGridProps {
|
||||
|
||||
interface MasonryItemProps {
|
||||
note: Note;
|
||||
onEdit: (note: Note) => void;
|
||||
onEdit: (note: Note, readOnly?: boolean) => void;
|
||||
onResize: () => void;
|
||||
}
|
||||
|
||||
function MasonryItem({ note, onEdit, onResize }: MasonryItemProps) {
|
||||
function getSizeClasses(size: string = 'small') {
|
||||
switch (size) {
|
||||
case 'medium':
|
||||
return 'w-full sm:w-full lg:w-2/3 xl:w-2/4 2xl:w-2/5';
|
||||
case 'large':
|
||||
return 'w-full';
|
||||
case 'small':
|
||||
default:
|
||||
return 'w-full sm:w-1/2 lg:w-1/3 xl:w-1/4 2xl:w-1/5';
|
||||
}
|
||||
}
|
||||
|
||||
const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize }: MasonryItemProps) {
|
||||
const resizeRef = useResizeObserver(() => {
|
||||
onResize();
|
||||
});
|
||||
|
||||
const sizeClasses = getSizeClasses(note.size);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="masonry-item absolute w-full sm:w-1/2 lg:w-1/3 xl:w-1/4 2xl:w-1/5 p-2"
|
||||
className={`masonry-item absolute p-2 ${sizeClasses}`}
|
||||
data-id={note.id}
|
||||
ref={resizeRef as any}
|
||||
>
|
||||
@@ -33,20 +47,28 @@ function MasonryItem({ note, onEdit, onResize }: MasonryItemProps) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}, (prev, next) => {
|
||||
// Custom comparison to avoid re-render on function prop changes if note data is same
|
||||
return prev.note === next.note;
|
||||
});
|
||||
|
||||
export function MasonryGrid({ notes }: MasonryGridProps) {
|
||||
const [editingNote, setEditingNote] = useState<Note | null>(null);
|
||||
const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null);
|
||||
const pinnedGridRef = useRef<HTMLDivElement>(null);
|
||||
const othersGridRef = useRef<HTMLDivElement>(null);
|
||||
const pinnedMuuri = useRef<any>(null);
|
||||
const othersMuuri = useRef<any>(null);
|
||||
const isDraggingRef = useRef(false);
|
||||
|
||||
const pinnedNotes = notes.filter(n => n.isPinned).sort((a, b) => a.order - b.order);
|
||||
const othersNotes = notes.filter(n => !n.isPinned).sort((a, b) => a.order - b.order);
|
||||
|
||||
const handleDragEnd = async (grid: any) => {
|
||||
if (!grid) return;
|
||||
|
||||
// Prevent layout refresh during server update
|
||||
isDraggingRef.current = true;
|
||||
|
||||
const items = grid.getItems();
|
||||
const ids = items
|
||||
.map((item: any) => item.getElement()?.getAttribute('data-id'))
|
||||
@@ -56,6 +78,11 @@ export function MasonryGrid({ notes }: MasonryGridProps) {
|
||||
await updateFullOrder(ids);
|
||||
} catch (error) {
|
||||
console.error('Failed to persist order:', error);
|
||||
} finally {
|
||||
// Reset after animation/server roundtrip
|
||||
setTimeout(() => {
|
||||
isDraggingRef.current = false;
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -82,8 +109,13 @@ export function MasonryGrid({ notes }: MasonryGridProps) {
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
// Detect if we are on a touch device (mobile behavior)
|
||||
const isMobile = window.matchMedia('(pointer: coarse)').matches;
|
||||
|
||||
const layoutOptions = {
|
||||
dragEnabled: true,
|
||||
// On mobile, restrict drag to handle to allow scrolling. On desktop, allow drag from anywhere.
|
||||
dragHandle: isMobile ? '.drag-handle' : undefined,
|
||||
dragContainer: document.body,
|
||||
dragStartPredicate: {
|
||||
distance: 10,
|
||||
@@ -125,10 +157,12 @@ export function MasonryGrid({ notes }: MasonryGridProps) {
|
||||
pinnedMuuri.current = null;
|
||||
othersMuuri.current = null;
|
||||
};
|
||||
}, [pinnedNotes.length, othersNotes.length]);
|
||||
}, [pinnedNotes.length > 0, othersNotes.length > 0]);
|
||||
|
||||
// Synchronize items when notes change (e.g. searching, adding)
|
||||
useEffect(() => {
|
||||
if (isDraggingRef.current) return;
|
||||
|
||||
if (pinnedMuuri.current) {
|
||||
pinnedMuuri.current.refreshItems().layout();
|
||||
}
|
||||
@@ -144,10 +178,10 @@ export function MasonryGrid({ notes }: MasonryGridProps) {
|
||||
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 px-2">Pinned</h2>
|
||||
<div ref={pinnedGridRef} className="relative min-h-[100px]">
|
||||
{pinnedNotes.map(note => (
|
||||
<MasonryItem
|
||||
key={note.id}
|
||||
note={note}
|
||||
onEdit={setEditingNote}
|
||||
<MasonryItem
|
||||
key={note.id}
|
||||
note={note}
|
||||
onEdit={(note, readOnly) => setEditingNote({ note, readOnly })}
|
||||
onResize={refreshLayout}
|
||||
/>
|
||||
))}
|
||||
@@ -162,10 +196,10 @@ export function MasonryGrid({ notes }: MasonryGridProps) {
|
||||
)}
|
||||
<div ref={othersGridRef} className="relative min-h-[100px]">
|
||||
{othersNotes.map(note => (
|
||||
<MasonryItem
|
||||
key={note.id}
|
||||
note={note}
|
||||
onEdit={setEditingNote}
|
||||
<MasonryItem
|
||||
key={note.id}
|
||||
note={note}
|
||||
onEdit={(note, readOnly) => setEditingNote({ note, readOnly })}
|
||||
onResize={refreshLayout}
|
||||
/>
|
||||
))}
|
||||
@@ -174,7 +208,11 @@ export function MasonryGrid({ notes }: MasonryGridProps) {
|
||||
)}
|
||||
|
||||
{editingNote && (
|
||||
<NoteEditor note={editingNote} onClose={() => setEditingNote(null)} />
|
||||
<NoteEditor
|
||||
note={editingNote.note}
|
||||
readOnly={editingNote.readOnly}
|
||||
onClose={() => setEditingNote(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<style jsx global>{`
|
||||
|
||||
Reference in New Issue
Block a user