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:
77
packages/admin/src/lib/api-client.ts
Normal file
77
packages/admin/src/lib/api-client.ts
Normal 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 }
|
||||
11
packages/admin/src/lib/query-client.ts
Normal file
11
packages/admin/src/lib/query-client.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30_000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
6
packages/admin/src/lib/utils.ts
Normal file
6
packages/admin/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user