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:
2026-01-09 22:13:49 +01:00
parent 3c4b9d6176
commit 640fcb26f7
218 changed files with 51363 additions and 902 deletions

View File

@@ -17,26 +17,31 @@ import {
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet'
import { Menu, Search, StickyNote, Tag, Moon, Sun, X, Bell, Trash2, Archive } from 'lucide-react'
import { Menu, Search, StickyNote, Tag, Moon, Sun, X, Bell, Trash2, Archive, Coffee } from 'lucide-react'
import Link from 'next/link'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { cn } from '@/lib/utils'
import { useLabels } from '@/context/LabelContext'
import { LabelManagementDialog } from './label-management-dialog'
import { LabelFilter } from './label-filter'
import { NotificationPanel } from './notification-panel'
import { UserNav } from './user-nav'
import { updateTheme } from '@/app/actions/profile'
interface HeaderProps {
selectedLabels?: string[]
selectedColor?: string | null
onLabelFilterChange?: (labels: string[]) => void
onColorFilterChange?: (color: string | null) => void
user?: any
}
export function Header({
selectedLabels = [],
selectedColor = null,
onLabelFilterChange,
onColorFilterChange
onColorFilterChange,
user
}: HeaderProps = {}) {
const [searchQuery, setSearchQuery] = useState('')
const [theme, setTheme] = useState<'light' | 'dark'>('light')
@@ -55,11 +60,12 @@ export function Header({
}, [currentSearch])
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light'
applyTheme(savedTheme)
}, [])
const savedTheme = user?.theme || localStorage.getItem('theme') || 'light'
// Don't persist on initial load to avoid unnecessary DB calls
applyTheme(savedTheme, false)
}, [user])
const applyTheme = (newTheme: string) => {
const applyTheme = async (newTheme: string, persist = true) => {
setTheme(newTheme as any)
localStorage.setItem('theme', newTheme)
@@ -75,6 +81,10 @@ export function Header({
document.documentElement.classList.add('dark')
}
}
if (persist && user) {
await updateTheme(newTheme)
}
}
const handleSearch = (query: string) => {
@@ -130,21 +140,59 @@ export function Header({
router.push(`/?${params.toString()}`)
}
const NavItem = ({ href, icon: Icon, label, active }: any) => (
<Link
href={href}
onClick={() => setIsSidebarOpen(false)}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-r-full text-sm font-medium transition-colors mr-2",
active
? "bg-amber-100 text-amber-900 dark:bg-amber-900/30 dark:text-amber-100"
: "hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-700 dark:text-gray-300"
)}
>
<Icon className={cn("h-5 w-5", active && "fill-current")} />
{label}
</Link>
)
const toggleLabelFilter = (labelName: string) => {
const newLabels = currentLabels.includes(labelName)
? currentLabels.filter(l => l !== labelName)
: [...currentLabels, labelName]
const params = new URLSearchParams(searchParams.toString())
if (newLabels.length > 0) {
params.set('labels', newLabels.join(','))
} else {
params.delete('labels')
}
router.push(`/?${params.toString()}`)
}
const NavItem = ({ href, icon: Icon, label, active, onClick }: any) => {
const content = (
<>
<Icon className={cn("h-5 w-5", active && "fill-current")} />
{label}
</>
)
if (onClick) {
return (
<button
onClick={onClick}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 rounded-r-full text-sm font-medium transition-colors mr-2 text-left",
active
? "bg-amber-100 text-amber-900 dark:bg-amber-900/30 dark:text-amber-100"
: "hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-700 dark:text-gray-300"
)}
>
{content}
</button>
)
}
return (
<Link
href={href}
onClick={() => setIsSidebarOpen(false)}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-r-full text-sm font-medium transition-colors mr-2",
active
? "bg-amber-100 text-amber-900 dark:bg-amber-900/30 dark:text-amber-100"
: "hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-700 dark:text-gray-300"
)}
>
{content}
</Link>
)
}
const hasActiveFilters = currentLabels.length > 0 || !!currentSearch || !!currentColor
@@ -188,26 +236,32 @@ export function Header({
{labels.map(label => (
<NavItem
key={label.id}
href={`/?labels=${encodeURIComponent(label.name)}`}
icon={Tag}
label={label.name}
active={currentLabels.includes(label.name)}
onClick={() => toggleLabelFilter(label.name)}
/>
))}
<div className="my-2 border-t border-gray-200 dark:border-zinc-800" />
<NavItem
href="/archive"
icon={Archive}
label="Archive"
active={pathname === '/archive'}
<NavItem
href="/archive"
icon={Archive}
label="Archive"
active={pathname === '/archive'}
/>
<NavItem
href="/trash"
icon={Trash2}
label="Trash"
active={pathname === '/trash'}
<NavItem
href="/trash"
icon={Trash2}
label="Trash"
active={pathname === '/trash'}
/>
<NavItem
href="/support"
icon={Coffee}
label="Support ☕"
active={pathname === '/support'}
/>
</div>
</SheetContent>
@@ -241,9 +295,7 @@ export function Header({
<div className="absolute right-0 top-0 h-full flex items-center pr-2">
<LabelFilter
selectedLabels={currentLabels}
selectedColor={currentColor || null}
onFilterChange={handleFilterChange}
onColorChange={handleColorChange}
/>
</div>
</div>
@@ -262,6 +314,9 @@ export function Header({
<DropdownMenuItem onClick={() => applyTheme('sepia')}>Sepia</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<NotificationPanel />
<UserNav user={user} />
</div>
</div>