From 16a8935958773c11bda97d87de2e1569997548d4 Mon Sep 17 00:00:00 2001 From: Asep Haryana Saputra <90584806+MythEclipse@users.noreply.github.com> Date: Fri, 22 May 2026 16:43:25 +0000 Subject: [PATCH] feat: add frontend production API client --- apps/web/src/lib/api-client.ts | 141 +++++++++++++++++++++++++++++-- apps/web/src/store/auth-store.ts | 12 +++ 2 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 apps/web/src/store/auth-store.ts diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 3964a2f..6691b6e 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -1,9 +1,15 @@ import type { - DiseaseSlug, + AuthMeResponse, + AuthRequest, + AuthResponse, + DiagnosisRecord, DiseaseCatalogItem, + DiseaseSlug, + ImageClassificationRecord, ManualClassificationRequest, ManualClassificationRecord, - ImageClassificationRecord, + RegisterRequest, + ReviewDiagnosisRequest, DashboardSummary, } from '@zeavis/shared'; @@ -11,7 +17,11 @@ const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? ''; async function fetchApi(endpoint: string, options?: RequestInit): Promise { const url = `${apiBaseUrl}${endpoint}`; - const response = await fetch(url, options); + const response = await fetch(url, { + credentials: 'include', + ...options, + headers: options?.headers, + }); if (!response.ok) { let errorMessage = `HTTP ${response.status}`; @@ -29,6 +39,32 @@ async function fetchApi(endpoint: string, options?: RequestInit): Promise } export const apiClient = { + // Auth methods + async getMe(): Promise { + return fetchApi('/api/v1/auth/me'); + }, + + async register(payload: RegisterRequest): Promise { + return fetchApi('/api/v1/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + }, + + async login(payload: AuthRequest): Promise { + return fetchApi('/api/v1/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + }, + + async logout(): Promise<{ ok: boolean }> { + return fetchApi('/api/v1/auth/logout', { method: 'POST' }); + }, + + // Disease catalog methods async getDiseases(): Promise { return fetchApi('/api/v1/diseases'); }, @@ -37,6 +73,7 @@ export const apiClient = { return fetchApi(`/api/v1/diseases/${slug}`); }, + // Manual classification methods async getManualClassifications(): Promise { return fetchApi('/api/v1/classifications/manual'); }, @@ -53,21 +90,111 @@ export const apiClient = { }); }, - async getImageClassifications(): Promise { - return fetchApi('/api/v1/classifications/image'); + // Diagnosis methods + async getDiagnoses(): Promise { + return fetchApi('/api/v1/diagnoses'); }, - async createImageClassification(file: File): Promise { + async getDiagnosis(id: string): Promise { + return fetchApi(`/api/v1/diagnoses/${id}`); + }, + + async createDiagnosis(file: File): Promise { const formData = new FormData(); formData.append('file', file); - return fetchApi('/api/v1/classifications/image', { + return fetchApi('/api/v1/diagnoses', { method: 'POST', body: formData, }); }, + // Expert review methods + async getExpertReviews(): Promise { + return fetchApi('/api/v1/expert/reviews'); + }, + + async reviewDiagnosis(id: string, payload: ReviewDiagnosisRequest): Promise { + return fetchApi(`/api/v1/expert/reviews/${id}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + }, + + // Dashboard methods async getDashboardSummary(): Promise { return fetchApi('/api/v1/dashboard/summary'); }, + + // Backwards-compatible aliases for old image classification methods + async getImageClassifications(): Promise { + const diagnoses = await this.getDiagnoses(); + return diagnoses.map((d) => ({ + id: d.id, + predictedDiseaseSlug: d.predictedDiseaseSlug || 'daun-sehat', + confidence: d.confidence || 0, + probabilities: d.predictions.map((p) => ({ + diseaseSlug: p.diseaseSlug || 'daun-sehat', + label: p.modelLabel, + confidence: p.confidence, + })), + imageUrl: d.imageUrl, + originalFileName: d.imageFileName, + uploaderPublicId: d.uploaderPublicId, + uploader: { + public_id: d.uploaderPublicId, + file_name: d.imageFileName, + mime_type: d.imageMimeType, + size_bytes: d.imageSizeBytes, + file_type: d.imageMimeType.split('/')[1] || 'unknown', + download_url: d.imageUrl, + }, + createdAt: d.createdAt, + disease: d.disease || { + slug: 'daun-sehat', + label: 'Daun Sehat', + commonName: 'Healthy Leaf', + description: '', + symptoms: [], + treatment: [], + riskLevel: 'low', + }, + })) as ImageClassificationRecord[]; + }, + + async createImageClassification(file: File): Promise { + const diagnosis = await this.createDiagnosis(file); + return { + id: diagnosis.id, + predictedDiseaseSlug: diagnosis.predictedDiseaseSlug || 'daun-sehat', + confidence: diagnosis.confidence || 0, + probabilities: diagnosis.predictions.map((p) => ({ + diseaseSlug: p.diseaseSlug || 'daun-sehat', + label: p.modelLabel, + confidence: p.confidence, + })), + imageUrl: diagnosis.imageUrl, + originalFileName: diagnosis.imageFileName, + uploaderPublicId: diagnosis.uploaderPublicId, + uploader: { + public_id: diagnosis.uploaderPublicId, + file_name: diagnosis.imageFileName, + mime_type: diagnosis.imageMimeType, + size_bytes: diagnosis.imageSizeBytes, + file_type: diagnosis.imageMimeType.split('/')[1] || 'unknown', + download_url: diagnosis.imageUrl, + }, + createdAt: diagnosis.createdAt, + disease: diagnosis.disease || { + slug: 'daun-sehat', + label: 'Daun Sehat', + commonName: 'Healthy Leaf', + description: '', + symptoms: [], + treatment: [], + riskLevel: 'low', + }, + } as ImageClassificationRecord; + }, }; diff --git a/apps/web/src/store/auth-store.ts b/apps/web/src/store/auth-store.ts new file mode 100644 index 0000000..aac63e5 --- /dev/null +++ b/apps/web/src/store/auth-store.ts @@ -0,0 +1,12 @@ +import { create } from 'zustand'; +import type { AuthUser } from '@zeavis/shared'; + +type AuthState = { + user: AuthUser | null; + setUser: (user: AuthUser | null) => void; +}; + +export const useAuthStore = create((set) => ({ + user: null, + setUser: (user) => set({ user }), +}));