jobhunt-platform/apps/web/src/api/index.ts
hermes bc81cb2398 T3: apps/web frontend shell (Vue 3 + Vite + TypeScript + Pinia + vue-router + Tailwind)
Implements the four views per the task card:

1. CV Editor (/cv):
   - Profile form (name/email/phone/location/headline/summary) with save
   - Section list editor: add/edit/delete sections with kind select,
     title/org/dates, bullet list editor with per-bullet AI-assist
     button calling POST /profile/sections/{id}/ai-assist, suggestions
     shown with accept/dismiss
   - Render CV button -> POST /profile/render-cv, shows URL link

2. Research (/research):
   - Table of postings (GET /postings) with company/title/location/
     source/fetched_at
   - Add by URL input -> POST /postings
   - Score button per row -> shows score result

3. Applications (/applications):
   - Kanban board grouped by state (10 columns per data-model states)
   - Cards show company/title/score
   - HTML5 drag-and-drop between columns -> POST /applications/{id}/transition
   - Optimistic update, revert on 409 with toast showing reason
   - Click card to navigate to detail view

4. Application detail (/applications/:id):
   - Posting info, state, score
   - Artifacts list
   - Cover-letter editor (textarea) -> save calls POST /applications/{id}/artifacts/
     cover-letter, critique rendered as cards with severity color coding
   - Approval widget: select artifact + action -> request approval ->
     I confirm button -> Send button (disabled until confirmed; shows
     409 errors as toasts)

Tech stack:
- Vue 3 + Vite + TypeScript (strict, noUnusedLocals/Parameters)
- Pinia for state (toast store)
- vue-router with lazy-loaded views
- Tailwind CSS configured locally (no CDN), PostCSS + autoprefixer
- API base from VITE_API_BASE defaulting to http://localhost:8000/api
- Typed API client module (src/api/index.ts) matching the contract
- Domain types (src/types/index.ts) from data-model.md

Tests (vitest, all passing):
- router.test.ts: router renders 3 tab links (CV, Research, Applications)
- Applications.test.ts: kanban groups cards by state from fixture
- ApplicationDetail.test.ts: Send disabled until confirmed; 409 error toast

Build: npm run build passes (vue-tsc --noEmit + vite build)
Tests: npm run test passes (4 tests, 3 files)
2026-07-30 18:00:55 +00:00

170 lines
No EOL
4.6 KiB
TypeScript

// API client module: thin typed fetch wrappers over the contract endpoints.
// Base URL from VITE_API_BASE, defaulting to http://localhost:8000/api.
import type {
AiAssistResponse,
Application,
Approval,
Artifact,
CoverLetterResponse,
CritiqueComment,
CvSection,
JobPosting,
Profile,
RenderCvResponse,
ScoreResponse
} from '@/types'
const API_BASE: string =
import.meta.env.VITE_API_BASE ?? 'http://localhost:8000/api'
export class HttpError extends Error {
status: number
body: unknown
constructor(status: number, body: unknown, message?: string) {
super(message ?? `HTTP ${status}`)
this.status = status
this.body = body
}
}
async function request<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
headers: { 'Content-Type': 'application/json', ...options.headers },
...options
})
if (!res.ok) {
let body: unknown = null
try {
body = await res.json()
} catch {
// non-JSON error body
}
throw new HttpError(res.status, body, `HTTP ${res.status}`)
}
return res.json() as Promise<T>
}
// --- Profile & CV ---
export function getProfile(): Promise<Profile> {
return request<Profile>('/profile')
}
export function updateProfile(data: Partial<Profile>): Promise<Profile> {
return request<Profile>('/profile', { method: 'PUT', body: JSON.stringify(data) })
}
export function getSections(): Promise<CvSection[]> {
return request<CvSection[]>('/profile/sections')
}
export function createSection(data: Partial<CvSection>): Promise<CvSection> {
return request<CvSection>('/profile/sections', { method: 'POST', body: JSON.stringify(data) })
}
export function updateSection(id: string, data: Partial<CvSection>): Promise<CvSection> {
return request<CvSection>(`/profile/sections/${id}`, { method: 'PUT', body: JSON.stringify(data) })
}
export function deleteSection(id: string): Promise<void> {
return request<void>(`/profile/sections/${id}`, { method: 'DELETE' })
}
export function aiAssist(sectionId: string, instruction: string): Promise<AiAssistResponse> {
return request<AiAssistResponse>(`/profile/sections/${sectionId}/ai-assist`, {
method: 'POST',
body: JSON.stringify({ instruction })
})
}
export function renderCv(): Promise<RenderCvResponse> {
return request<RenderCvResponse>('/profile/render-cv', { method: 'POST' })
}
// --- Job postings ---
export function getPostings(): Promise<JobPosting[]> {
return request<JobPosting[]>('/postings')
}
export function createPosting(url: string): Promise<JobPosting> {
return request<JobPosting>('/postings', { method: 'POST', body: JSON.stringify({ url }) })
}
export function scorePosting(postingId: string): Promise<ScoreResponse> {
return request<ScoreResponse>(`/postings/${postingId}/score`, { method: 'POST' })
}
// --- Applications ---
export function getApplications(): Promise<Application[]> {
return request<Application[]>('/applications')
}
export function transitionApplication(id: string, to: string): Promise<Application> {
return request<Application>(`/applications/${id}/transition`, {
method: 'POST',
body: JSON.stringify({ to })
})
}
export function getArtifacts(applicationId: string): Promise<Artifact[]> {
return request<Artifact[]>(`/applications/${applicationId}/artifacts`)
}
export function createCoverLetter(
applicationId: string,
letterText: string
): Promise<CoverLetterResponse> {
return request<CoverLetterResponse>(`/applications/${applicationId}/artifacts/cover-letter`, {
method: 'POST',
body: JSON.stringify({ letter_text: letterText })
})
}
// --- Approval & outbox ---
export function createApproval(
applicationId: string,
action: string,
artifactId: string
): Promise<Approval> {
return request<Approval>(`/applications/${applicationId}/approvals`, {
method: 'POST',
body: JSON.stringify({ action, artifact_id: artifactId })
})
}
export function confirmApproval(approvalId: string): Promise<Approval> {
return request<Approval>(`/approvals/${approvalId}/confirm`, { method: 'POST' })
}
export function rejectApproval(approvalId: string): Promise<Approval> {
return request<Approval>(`/approvals/${approvalId}/reject`, { method: 'POST' })
}
export function outboxSend(approvalId: string, payload: Record<string, unknown>): Promise<unknown> {
return request<unknown>('/outbox/send', {
method: 'POST',
body: JSON.stringify({ approval_id: approvalId, payload })
})
}
// Re-export types for convenience
export type {
AiAssistResponse,
Application,
Approval,
Artifact,
CoverLetterResponse,
CritiqueComment,
CvSection,
JobPosting,
Profile,
RenderCvResponse,
ScoreResponse
}