NeonClary's picture
Deploy tutorial feature and recent fixes
d8808e7 verified
Raw
History Blame Contribute Delete
4.06 kB
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import type { ReactNode } from 'react'
import { apiFetch } from '../api'
import type { CreationsStore } from '../lib/creations'
import {
ensureWorkshopSamplesInStore,
loadCreations,
mergeCreationsStores,
replaceCreationsStore,
syncCreationsToServerIfAuthed,
} from '../lib/creations'
export type User = {
id: string
email: string
firstName: string
lastName: string
}
type AuthCtx = {
user: User | null
token: string | null
loading: boolean
setAuth: (user: User, token: string) => void
signOut: () => void
refreshUser: () => Promise<void>
}
const Ctx = createContext<AuthCtx | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [token, setToken] = useState<string | null>(() => localStorage.getItem('authToken'))
const [loading, setLoading] = useState(true)
/** Merge server-saved workshop records with localStorage, then upload merged snapshot. */
const hydrateWorkshopCreations = useCallback(async () => {
const pr = await apiFetch('/api/users/me/profile')
if (!pr.ok) return
const profile = (await pr.json()) as { creations?: CreationsStore }
const raw = profile.creations
const remoteStore: CreationsStore = {
personas: Array.isArray(raw?.personas) ? raw!.personas : [],
advisorPanels: Array.isArray(raw?.advisorPanels) ? raw!.advisorPanels : [],
}
const localBefore = loadCreations()
const merged = mergeCreationsStores(localBefore, remoteStore)
const withSamples = ensureWorkshopSamplesInStore(merged)
replaceCreationsStore(withSamples, { skipServerSync: true })
void syncCreationsToServerIfAuthed()
}, [])
const refreshUser = useCallback(async () => {
const t = localStorage.getItem('authToken')
if (!t) {
setUser(null)
setToken(null)
return
}
const r = await apiFetch('/api/auth/me')
if (r.ok) {
const u = (await r.json()) as User
setUser(u)
setToken(t)
localStorage.setItem('user', JSON.stringify(u))
await hydrateWorkshopCreations()
} else {
localStorage.removeItem('authToken')
localStorage.removeItem('user')
setUser(null)
setToken(null)
}
}, [hydrateWorkshopCreations])
useEffect(() => {
let cancelled = false
;(async () => {
const t = localStorage.getItem('authToken')
if (!t) {
if (!cancelled) setLoading(false)
return
}
const r = await apiFetch('/api/auth/me')
if (cancelled) return
if (r.ok) {
const u = (await r.json()) as User
setUser(u)
setToken(t)
localStorage.setItem('user', JSON.stringify(u))
await hydrateWorkshopCreations()
} else {
localStorage.removeItem('authToken')
localStorage.removeItem('user')
setUser(null)
setToken(null)
}
setLoading(false)
})()
return () => {
cancelled = true
}
}, [hydrateWorkshopCreations])
const setAuth = useCallback(
(u: User, tok: string) => {
localStorage.setItem('authToken', tok)
localStorage.setItem('user', JSON.stringify(u))
setUser(u)
setToken(tok)
void hydrateWorkshopCreations()
},
[hydrateWorkshopCreations],
)
const signOut = useCallback(() => {
localStorage.removeItem('authToken')
localStorage.removeItem('user')
setUser(null)
setToken(null)
}, [])
const value = useMemo(
() => ({ user, token, loading, setAuth, signOut, refreshUser }),
[user, token, loading, setAuth, signOut, refreshUser],
)
return <Ctx.Provider value={value}>{children}</Ctx.Provider>
}
export function useAuth() {
const v = useContext(Ctx)
if (!v) throw new Error('useAuth outside AuthProvider')
return v
}