spine-framework-cortex 0.2.25 → 0.2.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/pages/community/CommunityPage.tsx +6 -15
- package/pages/courses/CoursesPage.tsx +2 -14
- package/pages/kb/KBEditorPage.tsx +1 -10
- package/pages/support/RedactionReview.tsx +1 -12
- package/pages/support/TicketDetailPage.tsx +25 -41
- package/hooks/useTypeRegistry.ts +0 -74
package/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "Cortex",
|
|
3
3
|
"slug": "cortex",
|
|
4
4
|
"description": "Unified workspace for CRM, Support, Community, and Knowledge Base",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.26",
|
|
6
6
|
"app_type": "full",
|
|
7
7
|
"route_prefix": "/cortex",
|
|
8
8
|
"required_roles": ["support", "support_admin"],
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
2
|
import { Send, Hash, MessageSquarePlus, AlertCircle } from 'lucide-react'
|
|
3
3
|
import { apiFetch } from '@core/lib/api'
|
|
4
|
-
import { getTypeIdAsync } from '../../hooks/useTypeRegistry'
|
|
5
4
|
import { Button } from '@core/components/ui/button'
|
|
6
5
|
import { Input } from '@core/components/ui/input'
|
|
7
6
|
import { Badge } from '@core/components/ui/badge'
|
|
@@ -21,28 +20,20 @@ function ThreadPane({ post }: { post: Post }) {
|
|
|
21
20
|
const [loading, setLoading] = useState(true)
|
|
22
21
|
const [replyText, setReplyText] = useState('')
|
|
23
22
|
const [sending, setSending] = useState(false)
|
|
24
|
-
const [threadTypeId, setThreadTypeId] = useState<string | null>(null)
|
|
25
|
-
const [messageTypeId, setMessageTypeId] = useState<string | null>(null)
|
|
26
23
|
|
|
27
24
|
useEffect(() => {
|
|
28
|
-
getTypeIdAsync('community_thread').then(id => setThreadTypeId(id))
|
|
29
|
-
getTypeIdAsync('community_reply').then(id => setMessageTypeId(id))
|
|
30
|
-
}, [])
|
|
31
|
-
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
if (!threadTypeId) return
|
|
34
25
|
setLoading(true)
|
|
35
|
-
apiFetch(`/api/admin-data?action=list&entity=threads&target_id=${post.id}&
|
|
26
|
+
apiFetch(`/api/admin-data?action=list&entity=threads&target_id=${post.id}&type_slug=community_thread&limit=5`)
|
|
36
27
|
.then(r => r.json()).then(j => {
|
|
37
28
|
const t = (j?.data ?? j)?.[0] ?? null
|
|
38
29
|
setThread(t)
|
|
39
30
|
if (t) return apiFetch(`/api/admin-data?action=list&entity=messages&thread_id=${t.id}&limit=100`).then(r => r.json())
|
|
40
31
|
return []
|
|
41
32
|
}).then(raw => { const msgs = raw?.data ?? raw; setMessages(Array.isArray(msgs) ? msgs : []) }).catch(() => {}).finally(() => setLoading(false))
|
|
42
|
-
}, [post.id
|
|
33
|
+
}, [post.id])
|
|
43
34
|
|
|
44
35
|
const handleSend = async () => {
|
|
45
|
-
if (!replyText.trim()
|
|
36
|
+
if (!replyText.trim()) return
|
|
46
37
|
setSending(true)
|
|
47
38
|
try {
|
|
48
39
|
let activeThread = thread
|
|
@@ -51,7 +42,7 @@ function ThreadPane({ post }: { post: Post }) {
|
|
|
51
42
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
52
43
|
body: JSON.stringify({
|
|
53
44
|
entity: 'threads',
|
|
54
|
-
|
|
45
|
+
type_slug: 'community_thread',
|
|
55
46
|
target_type: 'items',
|
|
56
47
|
target_id: post.id,
|
|
57
48
|
status: 'open',
|
|
@@ -67,7 +58,7 @@ function ThreadPane({ post }: { post: Post }) {
|
|
|
67
58
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
68
59
|
body: JSON.stringify({
|
|
69
60
|
thread_id: activeThread.id,
|
|
70
|
-
|
|
61
|
+
type_slug: 'community_reply',
|
|
71
62
|
content: replyText.trim(),
|
|
72
63
|
direction: 'outbound',
|
|
73
64
|
sequence: messages.reduce((max, m) => Math.max(max, m.sequence || 0), 0) + 1,
|
|
@@ -101,7 +92,7 @@ function ThreadPane({ post }: { post: Post }) {
|
|
|
101
92
|
<div className="border-t p-3 flex gap-2 shrink-0">
|
|
102
93
|
<Input placeholder="Reply as agent…" value={replyText} onChange={e => setReplyText(e.target.value)}
|
|
103
94
|
onKeyDown={e => e.key === 'Enter' && !e.shiftKey && handleSend()} className="flex-1" />
|
|
104
|
-
<Button size="icon" onClick={handleSend} disabled={sending || !replyText.trim() || !thread
|
|
95
|
+
<Button size="icon" onClick={handleSend} disabled={sending || !replyText.trim() || !thread}><Send size={14} /></Button>
|
|
105
96
|
</div>
|
|
106
97
|
</div>
|
|
107
98
|
)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { useState, useEffect
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
2
|
import { useNavigate } from 'react-router-dom'
|
|
3
3
|
import { apiFetch } from '@core/lib/api'
|
|
4
|
-
import { getTypeIdAsync } from '../../hooks/useTypeRegistry'
|
|
5
4
|
import { Button } from '@core/components/ui/button'
|
|
6
5
|
import { Input } from '@core/components/ui/input'
|
|
7
6
|
import { Badge } from '@core/components/ui/badge'
|
|
@@ -31,13 +30,6 @@ function DiscussionPanel({ lesson }: { lesson: Lesson }) {
|
|
|
31
30
|
const [loading, setLoading] = useState(true)
|
|
32
31
|
const [newMessage, setNewMessage] = useState('')
|
|
33
32
|
const [submitting, setSubmitting] = useState(false)
|
|
34
|
-
const messageTypeIdRef = useRef<string>('')
|
|
35
|
-
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
getTypeIdAsync('message').then(id => {
|
|
38
|
-
if (id) messageTypeIdRef.current = id
|
|
39
|
-
})
|
|
40
|
-
}, [])
|
|
41
33
|
|
|
42
34
|
useEffect(() => {
|
|
43
35
|
setLoading(true)
|
|
@@ -53,10 +45,6 @@ function DiscussionPanel({ lesson }: { lesson: Lesson }) {
|
|
|
53
45
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
54
46
|
e.preventDefault()
|
|
55
47
|
if (!newMessage.trim() || submitting) return
|
|
56
|
-
if (!messageTypeIdRef.current) {
|
|
57
|
-
console.error('Message type not loaded')
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
48
|
|
|
61
49
|
setSubmitting(true)
|
|
62
50
|
try {
|
|
@@ -83,7 +71,7 @@ function DiscussionPanel({ lesson }: { lesson: Lesson }) {
|
|
|
83
71
|
method: 'POST',
|
|
84
72
|
headers: { 'Content-Type': 'application/json' },
|
|
85
73
|
body: JSON.stringify({
|
|
86
|
-
|
|
74
|
+
type_slug: 'message',
|
|
87
75
|
thread_id: currentThread.id,
|
|
88
76
|
content: newMessage.trim(),
|
|
89
77
|
direction: 'inbound',
|
|
@@ -2,7 +2,6 @@ import { useState, useEffect } from 'react'
|
|
|
2
2
|
import { useParams, useNavigate } from 'react-router-dom'
|
|
3
3
|
import { useAppPath } from '@core/hooks/useAppPath'
|
|
4
4
|
import { apiFetch } from '@core/lib/api'
|
|
5
|
-
import { getTypeIdAsync } from '../../hooks/useTypeRegistry'
|
|
6
5
|
import { RichTextEditor } from '@core/components/ui/RichTextEditor'
|
|
7
6
|
import { Button } from '@core/components/ui/button'
|
|
8
7
|
import { Input } from '@core/components/ui/input'
|
|
@@ -73,7 +72,6 @@ export default function KBEditorPage() {
|
|
|
73
72
|
const [loading, setLoading] = useState(!isNew)
|
|
74
73
|
const [saving, setSaving] = useState(false)
|
|
75
74
|
const [error, setError] = useState<string | null>(null)
|
|
76
|
-
const [kbArticleTypeId, setKbArticleTypeId] = useState<string>('')
|
|
77
75
|
|
|
78
76
|
useEffect(() => {
|
|
79
77
|
if (isNew) return
|
|
@@ -97,23 +95,16 @@ export default function KBEditorPage() {
|
|
|
97
95
|
.finally(() => setLoading(false))
|
|
98
96
|
}, [id, isNew])
|
|
99
97
|
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
getTypeIdAsync('kb_article').then(id => {
|
|
102
|
-
if (id) setKbArticleTypeId(id)
|
|
103
|
-
})
|
|
104
|
-
}, [])
|
|
105
|
-
|
|
106
98
|
const handleSave = async (publish?: boolean) => {
|
|
107
99
|
if (!form.title.trim()) { setError('Title is required'); return }
|
|
108
100
|
if (!form.kb_type) { setError('KB Type is required'); return }
|
|
109
|
-
if (!kbArticleTypeId) { setError('Type not loaded'); return }
|
|
110
101
|
setSaving(true)
|
|
111
102
|
setError(null)
|
|
112
103
|
try {
|
|
113
104
|
const payload = {
|
|
114
105
|
title: form.title.trim(),
|
|
115
106
|
description: form.description,
|
|
116
|
-
|
|
107
|
+
type_slug: 'kb_article',
|
|
117
108
|
status: publish ? 'published' : form.status,
|
|
118
109
|
data: {
|
|
119
110
|
kb_type: form.kb_type,
|
|
@@ -2,7 +2,6 @@ import { useState, useEffect, useCallback } from 'react'
|
|
|
2
2
|
import { useParams, useNavigate } from 'react-router-dom'
|
|
3
3
|
import { useAppPath } from '@core/hooks/useAppPath'
|
|
4
4
|
import { apiFetch } from '@core/lib/api'
|
|
5
|
-
import { getTypeIdAsync } from '../../hooks/useTypeRegistry'
|
|
6
5
|
import { Button } from '@core/components/ui/button'
|
|
7
6
|
import { Badge } from '@core/components/ui/badge'
|
|
8
7
|
import { ScrollArea } from '@core/components/ui/scroll-area'
|
|
@@ -107,7 +106,6 @@ export default function RedactionReview() {
|
|
|
107
106
|
const [finalContent, setFinalContent] = useState('')
|
|
108
107
|
const [articleTitle, setArticleTitle] = useState('')
|
|
109
108
|
const [viewMode, setViewMode] = useState<'review' | 'final'>('review')
|
|
110
|
-
const [kbArticleTypeId, setKbArticleTypeId] = useState<string>('')
|
|
111
109
|
|
|
112
110
|
// Load ticket and trigger redaction analysis
|
|
113
111
|
useEffect(() => {
|
|
@@ -155,11 +153,6 @@ export default function RedactionReview() {
|
|
|
155
153
|
}
|
|
156
154
|
|
|
157
155
|
loadAndAnalyze()
|
|
158
|
-
|
|
159
|
-
// Load KB article type ID
|
|
160
|
-
getTypeIdAsync('kb_article').then(id => {
|
|
161
|
-
if (id) setKbArticleTypeId(id)
|
|
162
|
-
})
|
|
163
156
|
}, [id])
|
|
164
157
|
|
|
165
158
|
// Build content from ticket for redaction analysis.
|
|
@@ -264,10 +257,6 @@ export default function RedactionReview() {
|
|
|
264
257
|
// Publish KB article
|
|
265
258
|
const publishKB = async () => {
|
|
266
259
|
if (!id || !finalContent || !articleTitle) return
|
|
267
|
-
if (!kbArticleTypeId) {
|
|
268
|
-
console.error('KB article type not loaded')
|
|
269
|
-
return
|
|
270
|
-
}
|
|
271
260
|
|
|
272
261
|
setProcessing(true)
|
|
273
262
|
try {
|
|
@@ -276,7 +265,7 @@ export default function RedactionReview() {
|
|
|
276
265
|
method: 'POST',
|
|
277
266
|
headers: { 'Content-Type': 'application/json' },
|
|
278
267
|
body: JSON.stringify({
|
|
279
|
-
|
|
268
|
+
type_slug: 'kb_article',
|
|
280
269
|
title: articleTitle,
|
|
281
270
|
status: 'published',
|
|
282
271
|
description: finalContent,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { useParams, useNavigate } from 'react-router-dom'
|
|
3
3
|
import { useAppPath } from '@core/hooks/useAppPath'
|
|
4
4
|
import { apiFetch } from '@core/lib/api'
|
|
@@ -11,7 +11,6 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@core/components/ui/ta
|
|
|
11
11
|
import { Textarea } from '@core/components/ui/textarea'
|
|
12
12
|
import { ArrowLeft, Send, Lock, Globe, Bot, CheckCircle, AlertCircle, FileText, Building2, CreditCard, Package, BookOpen, Eye, EyeOff, Brain, Clock, TrendingUp, Tag as TagIcon, Sparkles } from 'lucide-react'
|
|
13
13
|
import { useAuth } from '@core/contexts/AuthContext'
|
|
14
|
-
import { getTypeIdAsync } from '../../hooks/useTypeRegistry'
|
|
15
14
|
|
|
16
15
|
interface Ticket {
|
|
17
16
|
id: string;
|
|
@@ -83,14 +82,12 @@ function MergedThreadPanel({
|
|
|
83
82
|
ticketId,
|
|
84
83
|
externalThread,
|
|
85
84
|
internalThread,
|
|
86
|
-
|
|
87
|
-
messageTypeId,
|
|
85
|
+
refreshThreads,
|
|
88
86
|
}: {
|
|
89
87
|
ticketId: string
|
|
90
88
|
externalThread: Thread | null
|
|
91
89
|
internalThread: Thread | null
|
|
92
|
-
|
|
93
|
-
messageTypeId: string | null
|
|
90
|
+
refreshThreads: () => Promise<void>
|
|
94
91
|
}) {
|
|
95
92
|
const [messages, setMessages] = useState<Message[]>([])
|
|
96
93
|
const [loading, setLoading] = useState(true)
|
|
@@ -137,32 +134,24 @@ function MergedThreadPanel({
|
|
|
137
134
|
try {
|
|
138
135
|
// Create internal thread if it doesn't exist
|
|
139
136
|
if (!targetThread && replyType === 'internal') {
|
|
140
|
-
if (!threadTypeId) {
|
|
141
|
-
console.error('support_thread type not resolved')
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
|
|
145
137
|
const threadRes = await apiFetch('/api/admin-data?action=create&entity=threads', {
|
|
146
138
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
147
139
|
body: JSON.stringify({
|
|
148
140
|
target_type: 'item',
|
|
149
141
|
target_id: ticketId,
|
|
150
142
|
visibility: 'internal',
|
|
151
|
-
|
|
143
|
+
type_slug: 'support_thread'
|
|
152
144
|
}),
|
|
153
145
|
})
|
|
154
146
|
const newThread = await threadRes.json()
|
|
155
147
|
if (newThread?.id) {
|
|
156
148
|
targetThread = newThread
|
|
157
|
-
|
|
158
|
-
const thr = await apiFetch(`/api/admin-data?action=list&entity=threads&target_id=${ticketId}&type_id=${threadTypeId}&limit=10`).then(r => r.json())
|
|
159
|
-
const threadList = thr?.data ?? thr
|
|
160
|
-
setThreads(Array.isArray(threadList) ? threadList : [])
|
|
149
|
+
await refreshThreads()
|
|
161
150
|
}
|
|
162
151
|
}
|
|
163
152
|
|
|
164
|
-
if (!targetThread
|
|
165
|
-
console.error('No thread
|
|
153
|
+
if (!targetThread) {
|
|
154
|
+
console.error('No thread available for message')
|
|
166
155
|
return
|
|
167
156
|
}
|
|
168
157
|
|
|
@@ -177,7 +166,7 @@ function MergedThreadPanel({
|
|
|
177
166
|
thread_id: targetThread.id,
|
|
178
167
|
content: reply.trim(),
|
|
179
168
|
direction: 'outbound',
|
|
180
|
-
|
|
169
|
+
type_slug: 'support_reply',
|
|
181
170
|
sequence: nextSeq,
|
|
182
171
|
visibility: replyType
|
|
183
172
|
}),
|
|
@@ -734,25 +723,25 @@ export default function TicketDetailPage() {
|
|
|
734
723
|
const [watcherId, setWatcherId] = useState<string | null>(null)
|
|
735
724
|
const [watchLoading, setWatchLoading] = useState(false)
|
|
736
725
|
const [analysisLoading, setAnalysisLoading] = useState(false)
|
|
737
|
-
const [threadTypeId, setThreadTypeId] = useState<string | null>(null)
|
|
738
|
-
const [messageTypeId, setMessageTypeId] = useState<string | null>(null)
|
|
739
|
-
|
|
740
|
-
useEffect(() => {
|
|
741
|
-
getTypeIdAsync('support_thread').then(id => setThreadTypeId(id))
|
|
742
|
-
getTypeIdAsync('support_reply').then(id => setMessageTypeId(id))
|
|
743
|
-
}, [])
|
|
744
726
|
|
|
745
|
-
|
|
746
|
-
if (!id
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
727
|
+
const loadTicketAndThreads = useCallback(async () => {
|
|
728
|
+
if (!id) return
|
|
729
|
+
setLoading(true)
|
|
730
|
+
try {
|
|
731
|
+
const [ir, thr] = await Promise.all([
|
|
732
|
+
apiFetch(`/api/admin-data?action=get&entity=items&id=${id}`).then(r => r.json()),
|
|
733
|
+
apiFetch(`/api/admin-data?action=list&entity=threads&target_id=${id}&type_slug=support_thread&limit=10`).then(r => r.json()),
|
|
734
|
+
])
|
|
751
735
|
setTicket(ir?.data ?? ir ?? null)
|
|
752
736
|
const threadList = thr?.data ?? thr
|
|
753
737
|
setThreads(Array.isArray(threadList) ? threadList : [])
|
|
754
|
-
}
|
|
755
|
-
|
|
738
|
+
} catch {
|
|
739
|
+
} finally {
|
|
740
|
+
setLoading(false)
|
|
741
|
+
}
|
|
742
|
+
}, [id])
|
|
743
|
+
|
|
744
|
+
useEffect(() => { loadTicketAndThreads() }, [loadTicketAndThreads])
|
|
756
745
|
|
|
757
746
|
// Check if current user is watching this ticket
|
|
758
747
|
useEffect(() => {
|
|
@@ -778,16 +767,12 @@ export default function TicketDetailPage() {
|
|
|
778
767
|
setIsWatching(false)
|
|
779
768
|
setWatcherId(null)
|
|
780
769
|
} else {
|
|
781
|
-
// Resolve watcher type_id via types API
|
|
782
|
-
const typeRes = await apiFetch('/api/types?kind=watcher&limit=1').then(r => r.json())
|
|
783
|
-
const types = Array.isArray(typeRes?.data) ? typeRes.data : Array.isArray(typeRes) ? typeRes : []
|
|
784
|
-
if (!types.length) { console.error('No watcher type found'); return }
|
|
785
770
|
const res = await apiFetch('/api/admin-data?action=create&entity=watchers', {
|
|
786
771
|
method: 'POST',
|
|
787
772
|
headers: { 'Content-Type': 'application/json' },
|
|
788
773
|
body: JSON.stringify({
|
|
789
774
|
entity: 'watchers',
|
|
790
|
-
|
|
775
|
+
type_slug: 'watcher',
|
|
791
776
|
target_type: 'item',
|
|
792
777
|
target_id: id,
|
|
793
778
|
person_id: user.id,
|
|
@@ -893,8 +878,7 @@ export default function TicketDetailPage() {
|
|
|
893
878
|
ticketId={id || ''}
|
|
894
879
|
externalThread={externalThread}
|
|
895
880
|
internalThread={internalThread}
|
|
896
|
-
|
|
897
|
-
messageTypeId={messageTypeId}
|
|
881
|
+
refreshThreads={loadTicketAndThreads}
|
|
898
882
|
/>
|
|
899
883
|
</div>
|
|
900
884
|
|
package/hooks/useTypeRegistry.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
2
|
-
import { supabase } from '@core/lib/supabase'
|
|
3
|
-
|
|
4
|
-
interface TypeRecord {
|
|
5
|
-
id: string
|
|
6
|
-
slug: string
|
|
7
|
-
name: string
|
|
8
|
-
description?: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// Module-level cache - persists across renders, shared across hooks
|
|
12
|
-
let typeCache: Map<string, TypeRecord> | null = null
|
|
13
|
-
let loadPromise: Promise<Map<string, TypeRecord>> | null = null
|
|
14
|
-
|
|
15
|
-
async function fetchTypes(): Promise<Map<string, TypeRecord>> {
|
|
16
|
-
if (typeCache) return typeCache
|
|
17
|
-
if (loadPromise) return loadPromise
|
|
18
|
-
|
|
19
|
-
loadPromise = (async () => {
|
|
20
|
-
try {
|
|
21
|
-
// Query Supabase directly - types is a config table, not exposed via admin-data
|
|
22
|
-
const { data: types, error } = await supabase
|
|
23
|
-
.from('types')
|
|
24
|
-
.select('id, slug, name, description')
|
|
25
|
-
.eq('is_active', true)
|
|
26
|
-
.limit(100)
|
|
27
|
-
|
|
28
|
-
if (error) throw error
|
|
29
|
-
|
|
30
|
-
typeCache = new Map((types || []).map(t => [t.slug, t]))
|
|
31
|
-
return typeCache
|
|
32
|
-
} catch (e) {
|
|
33
|
-
console.error('Failed to load types:', e)
|
|
34
|
-
typeCache = new Map() // Empty cache on error
|
|
35
|
-
return typeCache
|
|
36
|
-
} finally {
|
|
37
|
-
loadPromise = null
|
|
38
|
-
}
|
|
39
|
-
})()
|
|
40
|
-
|
|
41
|
-
return loadPromise
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function useTypeRegistry() {
|
|
45
|
-
const [types, setTypes] = useState<Map<string, TypeRecord>>(typeCache || new Map())
|
|
46
|
-
const [loading, setLoading] = useState(!typeCache)
|
|
47
|
-
const [error, setError] = useState<string | null>(null)
|
|
48
|
-
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
if (typeCache) return
|
|
51
|
-
|
|
52
|
-
fetchTypes()
|
|
53
|
-
.then(cache => {
|
|
54
|
-
setTypes(cache)
|
|
55
|
-
setLoading(false)
|
|
56
|
-
})
|
|
57
|
-
.catch(e => {
|
|
58
|
-
setError(e.message)
|
|
59
|
-
setLoading(false)
|
|
60
|
-
})
|
|
61
|
-
}, [])
|
|
62
|
-
|
|
63
|
-
const getTypeId = (slug: string): string | null => {
|
|
64
|
-
return types.get(slug)?.id || null
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return { types, loading, error, getTypeId }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Synchronous version for use inside async functions
|
|
71
|
-
export async function getTypeIdAsync(slug: string): Promise<string | null> {
|
|
72
|
-
const cache = await fetchTypes()
|
|
73
|
-
return cache.get(slug)?.id || null
|
|
74
|
-
}
|