feat(01-04): layout, routing, API hooks, tipi TypeScript, Dashboard, Settings
- types.ts: CalendarSlot, GeneratedPost, PostResult, JobStatus, Settings, SettingsStatus - api/client.ts: aggiunto apiGet, apiPost, apiPut, apiDownload, triggerDownload - api/hooks.ts: 10+ hooks TanStack Query (settings, generate, job polling, CSV export) - components/Layout.tsx + Sidebar.tsx: sidebar stone/amber palette con 4 nav link - pages/Dashboard.tsx: banner API key, quick actions link, step guide - pages/Settings.tsx: form completo (API key password, LLM select, brand, nicchie checkbox) - App.tsx: 5 route con BrowserRouter basename='/postgenerator', QueryClientProvider, Layout
This commit is contained in:
186
frontend/src/api/hooks.ts
Normal file
186
frontend/src/api/hooks.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* TanStack Query hooks per tutte le API calls del PostGenerator.
|
||||
*
|
||||
* Pattern generale:
|
||||
* - useQuery per lettura dati (GET)
|
||||
* - useMutation per scrittura dati (POST/PUT)
|
||||
* - Polling condizionale: refetchInterval attivo solo quando job in running
|
||||
*
|
||||
* Tutti gli endpoint usano API_BASE='/postgenerator/api' via apiGet/apiPost/apiPut/apiDownload.
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { apiDownload, apiGet, apiPost, apiPut, triggerDownload } from './client'
|
||||
import type {
|
||||
CalendarRequest,
|
||||
CalendarResponse,
|
||||
FormatsResponse,
|
||||
GenerateRequest,
|
||||
GenerateResponse,
|
||||
JobStatus,
|
||||
PostResult,
|
||||
Settings,
|
||||
SettingsStatus,
|
||||
} from '../types'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Carica le impostazioni correnti. api_key è mascherata (ultimi 4 char). */
|
||||
export function useSettings() {
|
||||
return useQuery<Settings>({
|
||||
queryKey: ['settings'],
|
||||
queryFn: () => apiGet<Settings>('/settings'),
|
||||
staleTime: 60_000,
|
||||
})
|
||||
}
|
||||
|
||||
/** Verifica se API key è configurata e quale modello è attivo. */
|
||||
export function useSettingsStatus() {
|
||||
return useQuery<SettingsStatus>({
|
||||
queryKey: ['settings', 'status'],
|
||||
queryFn: () => apiGet<SettingsStatus>('/settings/status'),
|
||||
staleTime: 30_000,
|
||||
refetchOnWindowFocus: true,
|
||||
})
|
||||
}
|
||||
|
||||
/** Aggiorna le impostazioni via PUT /api/settings. */
|
||||
export function useUpdateSettings() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<Settings, Error, Partial<Settings>>({
|
||||
mutationFn: (settings) => apiPut<Settings>('/settings', settings),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['settings'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Calendario
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Recupera i formati narrativi disponibili (7 formati). */
|
||||
export function useFormats() {
|
||||
return useQuery<FormatsResponse>({
|
||||
queryKey: ['calendar', 'formats'],
|
||||
queryFn: () => apiGet<FormatsResponse>('/calendar/formats'),
|
||||
staleTime: Infinity, // i formati non cambiano mai
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generazione bulk (async con job_id)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Avvia generazione batch di 13 post.
|
||||
* Risponde 202 con {job_id, message} — la generazione gira in background.
|
||||
* Il frontend deve fare polling su useJobStatus(jobId).
|
||||
*/
|
||||
export function useGenerateCalendar() {
|
||||
return useMutation<{ job_id: string; message: string }, Error, CalendarRequest>({
|
||||
mutationFn: (request) =>
|
||||
apiPost<{ job_id: string; message: string }>('/generate/bulk', request),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Polling stato job. refetchInterval attivo solo quando status='running'.
|
||||
* Smette di pollare automaticamente quando status diventa 'completed' o 'failed'.
|
||||
*/
|
||||
export function useJobStatus(jobId: string | null) {
|
||||
return useQuery<JobStatus>({
|
||||
queryKey: ['jobs', jobId, 'status'],
|
||||
queryFn: () => apiGet<JobStatus>(`/generate/job/${jobId}/status`),
|
||||
enabled: !!jobId,
|
||||
refetchInterval: (query) => {
|
||||
const status = query.state.data?.status
|
||||
// Polla ogni 2s solo quando il job è in esecuzione
|
||||
if (status === 'running') return 2000
|
||||
return false
|
||||
},
|
||||
staleTime: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Risultati completi di un job completato.
|
||||
* Usato in OutputReview dopo che il job ha status='completed'.
|
||||
*/
|
||||
export function useJobResults(jobId: string | null) {
|
||||
return useQuery<GenerateResponse>({
|
||||
queryKey: ['jobs', jobId, 'results'],
|
||||
queryFn: () => apiGet<GenerateResponse>(`/generate/job/${jobId}`),
|
||||
enabled: !!jobId,
|
||||
staleTime: 60_000,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generazione singolo post
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Genera un singolo post (sync — ritorna PostResult direttamente).
|
||||
* Usato per rigenerare post falliti o in GenerateSingle.
|
||||
*/
|
||||
export function useGenerateSingle() {
|
||||
return useMutation<PostResult, Error, GenerateRequest>({
|
||||
mutationFn: (request) => apiPost<PostResult>('/generate/single', request),
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Calendar generation (sync, per CalendarRequest -> CalendarResponse)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Genera calendario editoriale (sync).
|
||||
* Diverso da useGenerateCalendar: questo chiama POST /api/calendar/generate
|
||||
* che ritorna i slot del calendario senza generare il contenuto LLM.
|
||||
*/
|
||||
export function useGenerateCalendarPlan() {
|
||||
return useMutation<CalendarResponse, Error, CalendarRequest>({
|
||||
mutationFn: (request) =>
|
||||
apiPost<CalendarResponse>('/calendar/generate', request),
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Export CSV
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Scarica il CSV originale del job (GET /api/export/{jobId}/csv).
|
||||
* Triggera download browser automaticamente.
|
||||
*/
|
||||
export function useDownloadCsv() {
|
||||
return useMutation<void, Error, string>({
|
||||
mutationFn: async (jobId: string) => {
|
||||
const blob = await apiDownload(`/export/${jobId}/csv`, 'GET')
|
||||
triggerDownload(blob, `postgenerator-${jobId}.csv`)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Scarica il CSV con le modifiche inline (POST /api/export/{jobId}/csv).
|
||||
* Invia i risultati modificati e triggera download browser.
|
||||
*/
|
||||
export function useDownloadEditedCsv() {
|
||||
return useMutation<
|
||||
void,
|
||||
Error,
|
||||
{ jobId: string; results: PostResult[]; campagna: string }
|
||||
>({
|
||||
mutationFn: async ({ jobId, results, campagna }) => {
|
||||
const blob = await apiDownload(`/export/${jobId}/csv`, 'POST', {
|
||||
results,
|
||||
campagna,
|
||||
})
|
||||
triggerDownload(blob, `postgenerator-${jobId}-edited.csv`)
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user