Scaffold @forte/admin package with React, Vite, shadcn/ui, TanStack Router

Sets up the admin frontend with login page, auth guard, API client, Zustand
auth store, and all shadcn/ui components. Vite proxies /v1 to backend in dev.
This commit is contained in:
Ryan Moon
2026-03-28 07:35:12 -05:00
parent 01d6ff3fa3
commit e734ef4606
38 changed files with 3708 additions and 25 deletions

View File

@@ -0,0 +1,77 @@
import { useAuthStore } from '@/stores/auth.store'
class ApiError extends Error {
statusCode: number
details?: unknown
constructor(message: string, statusCode: number, details?: unknown) {
super(message)
this.name = 'ApiError'
this.statusCode = statusCode
this.details = details
}
}
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
const { token } = useAuthStore.getState()
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const res = await fetch(path, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
})
if (res.status === 401) {
useAuthStore.getState().logout()
window.location.href = '/login'
throw new ApiError('Unauthorized', 401)
}
const json = await res.json()
if (!res.ok) {
throw new ApiError(
json.error?.message ?? 'Request failed',
res.status,
json.error?.details,
)
}
return json as T
}
function buildQueryString(params?: Record<string, unknown>): string {
if (!params) return ''
const searchParams = new URLSearchParams()
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null && value !== '') {
searchParams.set(key, String(value))
}
}
const qs = searchParams.toString()
return qs ? `?${qs}` : ''
}
export const api = {
get: <T>(path: string, params?: Record<string, unknown>) =>
request<T>('GET', `${path}${buildQueryString(params)}`),
post: <T>(path: string, body: unknown) =>
request<T>('POST', path, body),
patch: <T>(path: string, body: unknown) =>
request<T>('PATCH', path, body),
del: <T>(path: string) =>
request<T>('DELETE', path),
}
export { ApiError }