Add ModeTemplate with Svelte 5 frontend and components
- Create reusable components: auth, api-client, shared-types - ModeTemplate with Hono backend and SvelteKit frontend - Auth store refactored to class-based Svelte 5 pattern - SSR-safe hydrate() pattern for localStorage access - Frontend builds successfully with Svelte 5 runes
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# API Client Component
|
||||
|
||||
Frontend helper for making authenticated requests to the backend.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { createApiClient } from './components/api-client';
|
||||
import { STORAGE_KEYS } from './components/auth';
|
||||
|
||||
// Create client with token getter
|
||||
const api = createApiClient({
|
||||
baseUrl: '/api', // or 'http://localhost:3005/api'
|
||||
getToken: () => localStorage.getItem(STORAGE_KEYS.PB_TOKEN),
|
||||
});
|
||||
|
||||
// Make requests
|
||||
const result = await api.get<User[]>('/users');
|
||||
|
||||
if (result.ok) {
|
||||
console.log(result.data); // User[]
|
||||
} else {
|
||||
console.error(result.error); // Error message
|
||||
}
|
||||
|
||||
// POST with body
|
||||
const created = await api.post<Job>('/jobs', {
|
||||
title: 'New Job',
|
||||
status: 'open',
|
||||
});
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
- `api.get<T>(path)` - GET request
|
||||
- `api.post<T>(path, body?)` - POST request
|
||||
- `api.put<T>(path, body?)` - PUT request
|
||||
- `api.patch<T>(path, body?)` - PATCH request
|
||||
- `api.delete<T>(path)` - DELETE request
|
||||
|
||||
## Response Type
|
||||
|
||||
```typescript
|
||||
interface ApiResponse<T> {
|
||||
ok: boolean; // true if status 2xx
|
||||
status: number; // HTTP status code
|
||||
data?: T; // Response data (if ok)
|
||||
error?: string; // Error message (if not ok)
|
||||
}
|
||||
```
|
||||
|
||||
## Copying to a Mode
|
||||
|
||||
1. Copy `components/api-client/` to your mode's `frontend/lib/api-client/`
|
||||
2. Update imports
|
||||
|
||||
## Dependencies
|
||||
|
||||
None - fully self-contained. No package.json needed - inherits from consuming project.
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* API Client Component
|
||||
*
|
||||
* Frontend helper for making authenticated requests to the backend.
|
||||
* Uses PocketBase token from auth session.
|
||||
*/
|
||||
|
||||
// Header constants (inline to avoid cross-component dependency)
|
||||
const HEADERS = {
|
||||
AUTH: 'Authorization',
|
||||
CONTENT_TYPE: 'Content-Type',
|
||||
} as const;
|
||||
|
||||
const CONTENT_TYPES = {
|
||||
JSON: 'application/json',
|
||||
} as const;
|
||||
|
||||
function authHeader(token: string): string {
|
||||
return `Bearer ${token}`;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = unknown> {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ApiClientConfig {
|
||||
baseUrl: string;
|
||||
getToken: () => string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an API client instance
|
||||
*/
|
||||
export function createApiClient(config: ApiClientConfig) {
|
||||
const { baseUrl, getToken } = config;
|
||||
|
||||
async function request<T>(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown
|
||||
): Promise<ApiResponse<T>> {
|
||||
const token = getToken();
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
[HEADERS.CONTENT_TYPE]: CONTENT_TYPES.JSON,
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers[HEADERS.AUTH] = authHeader(token);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
const data = response.headers.get('content-type')?.includes('application/json')
|
||||
? await response.json()
|
||||
: await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
error: typeof data === 'string' ? data : data.message || 'Request failed',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: response.status,
|
||||
data: data as T,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 0,
|
||||
error: err instanceof Error ? err.message : 'Network error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
get: <T>(path: string) => request<T>('GET', path),
|
||||
post: <T>(path: string, body?: unknown) => request<T>('POST', path, body),
|
||||
put: <T>(path: string, body?: unknown) => request<T>('PUT', path, body),
|
||||
patch: <T>(path: string, body?: unknown) => request<T>('PATCH', path, body),
|
||||
delete: <T>(path: string) => request<T>('DELETE', path),
|
||||
};
|
||||
}
|
||||
|
||||
export type ApiClient = ReturnType<typeof createApiClient>;
|
||||
@@ -0,0 +1,2 @@
|
||||
export { createApiClient } from './api-client';
|
||||
export type { ApiResponse, ApiClientConfig, ApiClient } from './api-client';
|
||||
Reference in New Issue
Block a user