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
146 lines
4.3 KiB
TypeScript
146 lines
4.3 KiB
TypeScript
'use client'
|
|
|
|
import { createContext, useContext, useState, useEffect, useCallback, useMemo, ReactNode } from 'react'
|
|
import { LabelColorName, LABEL_COLORS } from '@/lib/types'
|
|
import { getHashColor } from '@/lib/utils'
|
|
|
|
export interface Label {
|
|
id: string
|
|
name: string
|
|
color: LabelColorName
|
|
userId?: string | null
|
|
createdAt: Date
|
|
updatedAt: Date
|
|
}
|
|
|
|
interface LabelContextType {
|
|
labels: Label[]
|
|
loading: boolean
|
|
notebookId?: string | null
|
|
setNotebookId: (notebookId: string | null) => void
|
|
addLabel: (name: string, color?: LabelColorName, notebookId?: string | null) => Promise<void>
|
|
updateLabel: (id: string, updates: Partial<Pick<Label, 'name' | 'color'>>) => Promise<void>
|
|
deleteLabel: (id: string) => Promise<void>
|
|
getLabelColor: (name: string) => LabelColorName
|
|
refreshLabels: () => Promise<void>
|
|
}
|
|
|
|
const LabelContext = createContext<LabelContextType | undefined>(undefined)
|
|
|
|
export function LabelProvider({ children }: { children: ReactNode }) {
|
|
const [labels, setLabels] = useState<Label[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [notebookId, setNotebookId] = useState<string | null>(null)
|
|
|
|
const fetchLabels = useCallback(async (nbId: string | null) => {
|
|
try {
|
|
setLoading(true)
|
|
const url = new URL('/api/labels', window.location.origin)
|
|
if (nbId) {
|
|
url.searchParams.set('notebookId', nbId)
|
|
}
|
|
|
|
const response = await fetch(url.toString(), {
|
|
cache: 'no-store',
|
|
credentials: 'include'
|
|
})
|
|
const data = await response.json()
|
|
if (data.success && data.data) {
|
|
setLabels(data.data)
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch labels:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
fetchLabels(notebookId)
|
|
}, [notebookId, fetchLabels])
|
|
|
|
const addLabel = useCallback(async (name: string, color?: LabelColorName, labelNotebookId?: string | null) => {
|
|
try {
|
|
const labelColor = color || getHashColor(name);
|
|
const finalNotebookId = labelNotebookId || notebookId
|
|
|
|
const response = await fetch('/api/labels', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, color: labelColor, notebookId: finalNotebookId }),
|
|
})
|
|
const data = await response.json()
|
|
if (data.success && data.data) {
|
|
setLabels(prev => [...prev, data.data])
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to add label:', error)
|
|
throw error
|
|
}
|
|
}, [notebookId])
|
|
|
|
const updateLabel = useCallback(async (id: string, updates: Partial<Pick<Label, 'name' | 'color'>>) => {
|
|
try {
|
|
const response = await fetch(`/api/labels/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(updates),
|
|
})
|
|
const data = await response.json()
|
|
if (data.success && data.data) {
|
|
setLabels(prev => prev.map(label =>
|
|
label.id === id ? { ...label, ...data.data } : label
|
|
))
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to update label:', error)
|
|
throw error
|
|
}
|
|
}, [])
|
|
|
|
const deleteLabel = useCallback(async (id: string) => {
|
|
try {
|
|
const response = await fetch(`/api/labels/${id}`, {
|
|
method: 'DELETE',
|
|
})
|
|
if (response.ok) {
|
|
setLabels(prev => prev.filter(label => label.id !== id))
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to delete label:', error)
|
|
throw error
|
|
}
|
|
}, [])
|
|
|
|
const getLabelColor = useCallback((name: string): LabelColorName => {
|
|
const label = labels.find(l => l.name.toLowerCase() === name.toLowerCase())
|
|
return label?.color || 'gray'
|
|
}, [labels])
|
|
|
|
const refreshLabels = useCallback(async () => {
|
|
await fetchLabels(notebookId)
|
|
}, [fetchLabels, notebookId])
|
|
|
|
const value = useMemo<LabelContextType>(() => ({
|
|
labels,
|
|
loading,
|
|
notebookId,
|
|
setNotebookId,
|
|
addLabel,
|
|
updateLabel,
|
|
deleteLabel,
|
|
getLabelColor,
|
|
refreshLabels,
|
|
}), [labels, loading, notebookId, addLabel, updateLabel, deleteLabel, getLabelColor, refreshLabels])
|
|
|
|
return <LabelContext.Provider value={value}>{children}</LabelContext.Provider>
|
|
}
|
|
|
|
export function useLabels() {
|
|
const context = useContext(LabelContext)
|
|
if (context === undefined) {
|
|
throw new Error('useLabels must be used within a LabelProvider')
|
|
}
|
|
return context
|
|
}
|