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)
170 lines
No EOL
4.6 KiB
TypeScript
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
|
|
} |