feat: initial SPA frontend for network app

This commit is contained in:
2026-01-28 19:51:45 +00:00
commit 1afd5d5bac
35 changed files with 7071 additions and 0 deletions

198
src/lib/api.ts Normal file
View File

@@ -0,0 +1,198 @@
import type { Profile, Client, ClientCreate, Event, EventCreate, Email, EmailGenerate, User } from '@/types';
const API_BASE = import.meta.env.PROD
? 'https://api.donovankelly.xyz/api'
: '/api';
const AUTH_BASE = import.meta.env.PROD
? 'https://api.donovankelly.xyz'
: '';
class ApiClient {
private async fetch<T>(path: string, options: RequestInit = {}): Promise<T> {
const headers: HeadersInit = {
'Content-Type': 'application/json',
...options.headers,
};
const response = await fetch(`${API_BASE}${path}`, {
...options,
headers,
credentials: 'include',
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(error.error || error.message || 'Request failed');
}
const text = await response.text();
if (!text) return {} as T;
return JSON.parse(text);
}
// Auth
async login(email: string, password: string) {
const response = await fetch(`${AUTH_BASE}/api/auth/sign-in/email`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
credentials: 'include',
});
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Login failed' }));
throw new Error(error.message || 'Login failed');
}
return response.json();
}
async logout() {
await fetch(`${AUTH_BASE}/api/auth/sign-out`, {
method: 'POST',
credentials: 'include',
});
}
async getSession(): Promise<{ user: User } | null> {
try {
const response = await fetch(`${AUTH_BASE}/api/auth/get-session`, {
credentials: 'include',
});
if (!response.ok) return null;
return response.json();
} catch {
return null;
}
}
// Profile
async getProfile(): Promise<Profile> {
return this.fetch('/profile');
}
async updateProfile(data: Partial<Profile>): Promise<Profile> {
return this.fetch('/profile', {
method: 'PUT',
body: JSON.stringify(data),
});
}
// Clients
async getClients(params?: { search?: string; tag?: string }): Promise<Client[]> {
const searchParams = new URLSearchParams();
if (params?.search) searchParams.set('search', params.search);
if (params?.tag) searchParams.set('tag', params.tag);
const query = searchParams.toString();
return this.fetch(`/clients${query ? `?${query}` : ''}`);
}
async getClient(id: string): Promise<Client> {
return this.fetch(`/clients/${id}`);
}
async createClient(data: ClientCreate): Promise<Client> {
return this.fetch('/clients', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateClient(id: string, data: Partial<ClientCreate>): Promise<Client> {
return this.fetch(`/clients/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteClient(id: string): Promise<void> {
await this.fetch(`/clients/${id}`, { method: 'DELETE' });
}
async markContacted(id: string): Promise<Client> {
return this.fetch(`/clients/${id}/contacted`, { method: 'POST' });
}
// Events
async getEvents(params?: { clientId?: string; type?: string; upcoming?: number }): Promise<Event[]> {
const searchParams = new URLSearchParams();
if (params?.clientId) searchParams.set('clientId', params.clientId);
if (params?.type) searchParams.set('type', params.type);
if (params?.upcoming) searchParams.set('upcoming', String(params.upcoming));
const query = searchParams.toString();
return this.fetch(`/events${query ? `?${query}` : ''}`);
}
async getEvent(id: string): Promise<Event> {
return this.fetch(`/events/${id}`);
}
async createEvent(data: EventCreate): Promise<Event> {
return this.fetch('/events', {
method: 'POST',
body: JSON.stringify(data),
});
}
async updateEvent(id: string, data: Partial<EventCreate>): Promise<Event> {
return this.fetch(`/events/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async deleteEvent(id: string): Promise<void> {
await this.fetch(`/events/${id}`, { method: 'DELETE' });
}
async syncAllEvents(): Promise<void> {
await this.fetch('/events/sync-all', { method: 'POST' });
}
async syncClientEvents(clientId: string): Promise<void> {
await this.fetch(`/events/sync/${clientId}`, { method: 'POST' });
}
// Emails
async getEmails(params?: { status?: string; clientId?: string }): Promise<Email[]> {
const searchParams = new URLSearchParams();
if (params?.status) searchParams.set('status', params.status);
if (params?.clientId) searchParams.set('clientId', params.clientId);
const query = searchParams.toString();
return this.fetch(`/emails${query ? `?${query}` : ''}`);
}
async getEmail(id: string): Promise<Email> {
return this.fetch(`/emails/${id}`);
}
async generateEmail(data: EmailGenerate): Promise<Email> {
return this.fetch('/emails/generate', {
method: 'POST',
body: JSON.stringify(data),
});
}
async generateBirthdayEmail(clientId: string, provider?: string): Promise<Email> {
return this.fetch('/emails/generate-birthday', {
method: 'POST',
body: JSON.stringify({ clientId, provider }),
});
}
async updateEmail(id: string, data: { subject?: string; content?: string }): Promise<Email> {
return this.fetch(`/emails/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async sendEmail(id: string): Promise<Email> {
return this.fetch(`/emails/${id}/send`, { method: 'POST' });
}
async deleteEmail(id: string): Promise<void> {
await this.fetch(`/emails/${id}`, { method: 'DELETE' });
}
}
export const api = new ApiClient();