## 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
83 lines
3.6 KiB
TypeScript
83 lines
3.6 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { deleteUser, updateUserRole } from '@/app/actions/admin'
|
|
import { toast } from 'sonner'
|
|
import { Trash2, Shield, ShieldOff } from 'lucide-react'
|
|
import { format } from 'date-fns'
|
|
|
|
export function UserList({ initialUsers }: { initialUsers: any[] }) {
|
|
|
|
// Optimistic update could be implemented here, but standard is fine for admin
|
|
const handleDelete = async (id: string) => {
|
|
if (!confirm('Are you sure? This action cannot be undone.')) return
|
|
try {
|
|
await deleteUser(id)
|
|
toast.success('User deleted')
|
|
} catch (e) {
|
|
toast.error('Failed to delete')
|
|
}
|
|
}
|
|
|
|
const handleRoleToggle = async (user: any) => {
|
|
const newRole = user.role === 'ADMIN' ? 'USER' : 'ADMIN'
|
|
try {
|
|
await updateUserRole(user.id, newRole)
|
|
toast.success(`User role updated to ${newRole}`)
|
|
} catch (e) {
|
|
toast.error('Failed to update role')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="w-full overflow-auto">
|
|
<table className="w-full caption-bottom text-sm text-left">
|
|
<thead className="[&_tr]:border-b">
|
|
<tr className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
|
|
<th className="h-12 px-4 align-middle font-medium text-muted-foreground">Name</th>
|
|
<th className="h-12 px-4 align-middle font-medium text-muted-foreground">Email</th>
|
|
<th className="h-12 px-4 align-middle font-medium text-muted-foreground">Role</th>
|
|
<th className="h-12 px-4 align-middle font-medium text-muted-foreground">Created At</th>
|
|
<th className="h-12 px-4 align-middle font-medium text-muted-foreground text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="[&_tr:last-child]:border-0">
|
|
{initialUsers.map((user) => (
|
|
<tr key={user.id} className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
|
|
<td className="p-4 align-middle font-medium">{user.name || 'N/A'}</td>
|
|
<td className="p-4 align-middle">{user.email}</td>
|
|
<td className="p-4 align-middle">
|
|
<span className={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${user.role === 'ADMIN' ? 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80' : 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80'}`}>
|
|
{user.role}
|
|
</span>
|
|
</td>
|
|
<td className="p-4 align-middle">{format(new Date(user.createdAt), 'PP')}</td>
|
|
<td className="p-4 align-middle text-right">
|
|
<div className="flex justify-end gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleRoleToggle(user)}
|
|
title={user.role === 'ADMIN' ? "Demote to User" : "Promote to Admin"}
|
|
>
|
|
{user.role === 'ADMIN' ? <ShieldOff className="h-4 w-4" /> : <Shield className="h-4 w-4" />}
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleDelete(user.id)}
|
|
className="text-red-600 hover:text-red-900"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)
|
|
}
|