fix: resolve React Error #310 and refactor admin section
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled
- Fix React bug #33580: remove Suspense boundaries co-located with Link components - Delete settings/loading.tsx and admin/loading.tsx (root cause of race condition) - Convert all admin navigation from Next.js Link to anchor tags - Move admin pages to dedicated (admin) route group - Add AdminHeader matching main header visual design - Add AdminSidebar with anchor-based navigation - Add /api/admin/models route handler (replaces server actions for GET) - Add /api/debug/client-error for server-side browser error reporting - Add useNoteRefreshOptional() to fix crash in AdminHeader - Hide Admin Dashboard menu for non-admin users - Change app icons from yellow to blue (#3A7CA5) matching brand primary - Fix admin search bar width to match main header Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'
|
||||
import { createContext, useContext, useState, useEffect, useCallback, useMemo, ReactNode } from 'react'
|
||||
import { LabelColorName, LABEL_COLORS } from '@/lib/types'
|
||||
import { getHashColor } from '@/lib/utils'
|
||||
|
||||
@@ -32,13 +32,12 @@ export function LabelProvider({ children }: { children: ReactNode }) {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [notebookId, setNotebookId] = useState<string | null>(null)
|
||||
|
||||
const fetchLabels = async () => {
|
||||
const fetchLabels = useCallback(async (nbId: string | null) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
// Build URL with notebookId filter
|
||||
const url = new URL('/api/labels', window.location.origin)
|
||||
if (notebookId) {
|
||||
url.searchParams.set('notebookId', notebookId)
|
||||
if (nbId) {
|
||||
url.searchParams.set('notebookId', nbId)
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
@@ -54,14 +53,13 @@ export function LabelProvider({ children }: { children: ReactNode }) {
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Re-fetch labels when notebookId changes
|
||||
useEffect(() => {
|
||||
fetchLabels()
|
||||
}, [notebookId])
|
||||
fetchLabels(notebookId)
|
||||
}, [notebookId, fetchLabels])
|
||||
|
||||
const addLabel = async (name: string, color?: LabelColorName, labelNotebookId?: string | null) => {
|
||||
const addLabel = useCallback(async (name: string, color?: LabelColorName, labelNotebookId?: string | null) => {
|
||||
try {
|
||||
const labelColor = color || getHashColor(name);
|
||||
const finalNotebookId = labelNotebookId || notebookId
|
||||
@@ -79,9 +77,9 @@ export function LabelProvider({ children }: { children: ReactNode }) {
|
||||
console.error('Failed to add label:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}, [notebookId])
|
||||
|
||||
const updateLabel = async (id: string, updates: Partial<Pick<Label, 'name' | 'color'>>) => {
|
||||
const updateLabel = useCallback(async (id: string, updates: Partial<Pick<Label, 'name' | 'color'>>) => {
|
||||
try {
|
||||
const response = await fetch(`/api/labels/${id}`, {
|
||||
method: 'PUT',
|
||||
@@ -90,7 +88,7 @@ export function LabelProvider({ children }: { children: ReactNode }) {
|
||||
})
|
||||
const data = await response.json()
|
||||
if (data.success && data.data) {
|
||||
setLabels(prev => prev.map(label =>
|
||||
setLabels(prev => prev.map(label =>
|
||||
label.id === id ? { ...label, ...data.data } : label
|
||||
))
|
||||
}
|
||||
@@ -98,9 +96,9 @@ export function LabelProvider({ children }: { children: ReactNode }) {
|
||||
console.error('Failed to update label:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const deleteLabel = async (id: string) => {
|
||||
const deleteLabel = useCallback(async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/labels/${id}`, {
|
||||
method: 'DELETE',
|
||||
@@ -112,18 +110,18 @@ export function LabelProvider({ children }: { children: ReactNode }) {
|
||||
console.error('Failed to delete label:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getLabelColorHelper = (name: string): LabelColorName => {
|
||||
const getLabelColor = useCallback((name: string): LabelColorName => {
|
||||
const label = labels.find(l => l.name.toLowerCase() === name.toLowerCase())
|
||||
return label?.color || 'gray'
|
||||
}
|
||||
}, [labels])
|
||||
|
||||
const refreshLabels = async () => {
|
||||
await fetchLabels()
|
||||
}
|
||||
const refreshLabels = useCallback(async () => {
|
||||
await fetchLabels(notebookId)
|
||||
}, [fetchLabels, notebookId])
|
||||
|
||||
const value: LabelContextType = {
|
||||
const value = useMemo<LabelContextType>(() => ({
|
||||
labels,
|
||||
loading,
|
||||
notebookId,
|
||||
@@ -131,9 +129,9 @@ export function LabelProvider({ children }: { children: ReactNode }) {
|
||||
addLabel,
|
||||
updateLabel,
|
||||
deleteLabel,
|
||||
getLabelColor: getLabelColorHelper,
|
||||
getLabelColor,
|
||||
refreshLabels,
|
||||
}
|
||||
}), [labels, loading, notebookId, addLabel, updateLabel, deleteLabel, getLabelColor, refreshLabels])
|
||||
|
||||
return <LabelContext.Provider value={value}>{children}</LabelContext.Provider>
|
||||
}
|
||||
@@ -144,4 +142,4 @@ export function useLabels() {
|
||||
throw new Error('useLabels must be used within a LabelProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useState, useCallback } from 'react'
|
||||
import { createContext, useContext, useState, useCallback, useMemo } from 'react'
|
||||
|
||||
interface NoteRefreshContextType {
|
||||
refreshKey: number
|
||||
@@ -16,8 +16,10 @@ export function NoteRefreshProvider({ children }: { children: React.ReactNode })
|
||||
setRefreshKey(prev => prev + 1)
|
||||
}, [])
|
||||
|
||||
const value = useMemo(() => ({ refreshKey, triggerRefresh }), [refreshKey, triggerRefresh])
|
||||
|
||||
return (
|
||||
<NoteRefreshContext.Provider value={{ refreshKey, triggerRefresh }}>
|
||||
<NoteRefreshContext.Provider value={value}>
|
||||
{children}
|
||||
</NoteRefreshContext.Provider>
|
||||
)
|
||||
@@ -30,3 +32,12 @@ export function useNoteRefresh() {
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as useNoteRefresh but tolerates being called outside the provider
|
||||
* (e.g. shared header rendered in admin pages). Returns a no-op when absent.
|
||||
*/
|
||||
export function useNoteRefreshOptional(): NoteRefreshContextType {
|
||||
const context = useContext(NoteRefreshContext)
|
||||
return context ?? { refreshKey: 0, triggerRefresh: () => {} }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useState, useCallback, ReactNode } from 'react'
|
||||
import { createContext, useContext, useState, useCallback, useMemo, ReactNode } from 'react'
|
||||
|
||||
interface NotebookDragContextValue {
|
||||
draggedNoteId: string | null
|
||||
@@ -46,18 +46,18 @@ export function NotebookDragProvider({ children }: NotebookDragProviderProps) {
|
||||
const isDragging = draggedNoteId !== null
|
||||
const isDragOver = dragOverNotebookId !== null
|
||||
|
||||
const value = useMemo(() => ({
|
||||
draggedNoteId,
|
||||
dragOverNotebookId,
|
||||
startDrag,
|
||||
endDrag,
|
||||
dragOver,
|
||||
isDragging,
|
||||
isDragOver,
|
||||
}), [draggedNoteId, dragOverNotebookId, startDrag, endDrag, dragOver, isDragging, isDragOver])
|
||||
|
||||
return (
|
||||
<NotebookDragContext.Provider
|
||||
value={{
|
||||
draggedNoteId,
|
||||
dragOverNotebookId,
|
||||
startDrag,
|
||||
endDrag,
|
||||
dragOver,
|
||||
isDragging,
|
||||
isDragOver,
|
||||
}}
|
||||
>
|
||||
<NotebookDragContext.Provider value={value}>
|
||||
{children}
|
||||
</NotebookDragContext.Provider>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user