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:
Michele
2026-03-08 02:23:55 +01:00
parent 60b46cb5c1
commit 738a877d39
11 changed files with 941 additions and 14 deletions

186
frontend/src/api/hooks.ts Normal file
View 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`)
},
})
}