feat(03-01): pagina SwipeFile UI + TanStack hooks + navigazione
- frontend/src/types.ts: aggiunge SwipeItem, SwipeItemCreate, SwipeItemUpdate, SwipeListResponse - frontend/src/api/hooks.ts: aggiunge useSwipeItems, useAddSwipeItem, useUpdateSwipeItem, useDeleteSwipeItem - frontend/src/pages/SwipeFile.tsx: pagina completa con form aggiunta, filtro nicchia, modifica inline, eliminazione con dialog conferma, data relativa - frontend/src/components/Sidebar.tsx: aggiunge voce "Swipe File" con icona Lightbulb - frontend/src/App.tsx: registra route /swipe-file - TypeScript + build Vite: nessun errore
This commit is contained in:
@@ -7,6 +7,7 @@ import { GenerateSingle } from './pages/GenerateSingle'
|
||||
import { OutputReview } from './pages/OutputReview'
|
||||
import { PromptEditor } from './pages/PromptEditor'
|
||||
import { Settings } from './pages/Settings'
|
||||
import { SwipeFile } from './pages/SwipeFile'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -29,6 +30,7 @@ function App() {
|
||||
<Route path="/genera-singolo" element={<GenerateSingle />} />
|
||||
<Route path="/risultati/:jobId" element={<OutputReview />} />
|
||||
<Route path="/prompt-editor" element={<PromptEditor />} />
|
||||
<Route path="/swipe-file" element={<SwipeFile />} />
|
||||
<Route path="/impostazioni" element={<Settings />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { apiDownload, apiGet, apiPost, apiPut, triggerDownload } from './client'
|
||||
import { apiFetch, apiDownload, apiGet, apiPost, apiPut, triggerDownload } from './client'
|
||||
import type {
|
||||
CalendarRequest,
|
||||
CalendarResponse,
|
||||
@@ -23,6 +23,10 @@ import type {
|
||||
PromptListResponse,
|
||||
Settings,
|
||||
SettingsStatus,
|
||||
SwipeItem,
|
||||
SwipeItemCreate,
|
||||
SwipeItemUpdate,
|
||||
SwipeListResponse,
|
||||
} from '../types'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -234,3 +238,49 @@ export function useResetPrompt() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Swipe File
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Lista tutte le idee salvate nello Swipe File (ordine: piu' recenti prima). */
|
||||
export function useSwipeItems() {
|
||||
return useQuery<SwipeListResponse>({
|
||||
queryKey: ['swipe'],
|
||||
queryFn: () => apiGet<SwipeListResponse>('/swipe/'),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
}
|
||||
|
||||
/** Aggiunge una nuova idea allo Swipe File. */
|
||||
export function useAddSwipeItem() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<SwipeItem, Error, SwipeItemCreate>({
|
||||
mutationFn: (item) => apiPost<SwipeItem>('/swipe/', item),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['swipe'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** Aggiorna una voce esistente dello Swipe File. */
|
||||
export function useUpdateSwipeItem() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<SwipeItem, Error, { id: string; data: SwipeItemUpdate }>({
|
||||
mutationFn: ({ id, data }) => apiPut<SwipeItem>(`/swipe/${id}`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['swipe'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** Elimina una voce dallo Swipe File. */
|
||||
export function useDeleteSwipeItem() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<{ deleted: boolean }, Error, string>({
|
||||
mutationFn: (id) => apiFetch<{ deleted: boolean }>(`/swipe/${id}`, { method: 'DELETE' }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['swipe'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Usa NavLink da react-router-dom per active state automatico.
|
||||
*/
|
||||
|
||||
import { BarChart2, Calendar, FileText, Home, Pencil, Settings } from 'lucide-react'
|
||||
import { BarChart2, Calendar, FileText, Home, Lightbulb, Pencil, Settings } from 'lucide-react'
|
||||
import { NavLink } from 'react-router-dom'
|
||||
|
||||
interface NavItem {
|
||||
@@ -36,6 +36,11 @@ const navItems: NavItem[] = [
|
||||
label: 'Prompt Editor',
|
||||
icon: <Pencil size={18} />,
|
||||
},
|
||||
{
|
||||
to: '/swipe-file',
|
||||
label: 'Swipe File',
|
||||
icon: <Lightbulb size={18} />,
|
||||
},
|
||||
{
|
||||
to: '/impostazioni',
|
||||
label: 'Impostazioni',
|
||||
|
||||
490
frontend/src/pages/SwipeFile.tsx
Normal file
490
frontend/src/pages/SwipeFile.tsx
Normal file
@@ -0,0 +1,490 @@
|
||||
/**
|
||||
* Pagina Swipe File — Cattura veloce di idee e topic per content marketing.
|
||||
*
|
||||
* Features:
|
||||
* - Form aggiunta rapida inline (sempre visibile in cima alla lista)
|
||||
* - Lista idee ordinata cronologicamente (piu' recenti prima)
|
||||
* - Filtro rapido per nicchia via chip/pill cliccabili
|
||||
* - Modifica inline: click su Pencil → i campi diventano input, Salva/Annulla
|
||||
* - Eliminazione con dialog di conferma
|
||||
* - Badge "Usato" per voci gia' utilizzate in un calendario
|
||||
* - Data relativa ("3 giorni fa", "2 ore fa")
|
||||
*/
|
||||
|
||||
import {
|
||||
Check,
|
||||
CheckCircle,
|
||||
Filter,
|
||||
Lightbulb,
|
||||
Pencil,
|
||||
Plus,
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
useAddSwipeItem,
|
||||
useDeleteSwipeItem,
|
||||
useSwipeItems,
|
||||
useUpdateSwipeItem,
|
||||
} from '../api/hooks'
|
||||
import type { SwipeItem, SwipeItemUpdate } from '../types'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: data relativa
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function relativeTime(isoDate: string): string {
|
||||
const now = Date.now()
|
||||
const then = new Date(isoDate).getTime()
|
||||
const diffMs = now - then
|
||||
const diffSec = Math.floor(diffMs / 1000)
|
||||
const diffMin = Math.floor(diffSec / 60)
|
||||
const diffHr = Math.floor(diffMin / 60)
|
||||
const diffDay = Math.floor(diffHr / 24)
|
||||
const diffWeek = Math.floor(diffDay / 7)
|
||||
const diffMonth = Math.floor(diffDay / 30)
|
||||
|
||||
if (diffSec < 60) return 'adesso'
|
||||
if (diffMin < 60) return `${diffMin} min fa`
|
||||
if (diffHr < 24) return `${diffHr} ${diffHr === 1 ? 'ora' : 'ore'} fa`
|
||||
if (diffDay < 7) return `${diffDay} ${diffDay === 1 ? 'giorno' : 'giorni'} fa`
|
||||
if (diffWeek < 5) return `${diffWeek} ${diffWeek === 1 ? 'settimana' : 'settimane'} fa`
|
||||
return `${diffMonth} ${diffMonth === 1 ? 'mese' : 'mesi'} fa`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sotto-componente: card singola idea
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface SwipeCardProps {
|
||||
item: SwipeItem
|
||||
isEditing: boolean
|
||||
editForm: { topic: string; nicchia: string; note: string }
|
||||
deleteConfirm: boolean
|
||||
onStartEdit: () => void
|
||||
onEditFormChange: (field: 'topic' | 'nicchia' | 'note', value: string) => void
|
||||
onSaveEdit: () => void
|
||||
onCancelEdit: () => void
|
||||
onRequestDelete: () => void
|
||||
onConfirmDelete: () => void
|
||||
onCancelDelete: () => void
|
||||
isSaving: boolean
|
||||
isDeleting: boolean
|
||||
}
|
||||
|
||||
function SwipeCard({
|
||||
item,
|
||||
isEditing,
|
||||
editForm,
|
||||
deleteConfirm,
|
||||
onStartEdit,
|
||||
onEditFormChange,
|
||||
onSaveEdit,
|
||||
onCancelEdit,
|
||||
onRequestDelete,
|
||||
onConfirmDelete,
|
||||
onCancelDelete,
|
||||
isSaving,
|
||||
isDeleting,
|
||||
}: SwipeCardProps) {
|
||||
return (
|
||||
<div className="bg-stone-800 border border-stone-700 rounded-xl p-4 space-y-3">
|
||||
{isEditing ? (
|
||||
/* Modalita' modifica inline */
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs text-stone-400 mb-1">Topic *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.topic}
|
||||
onChange={(e) => onEditFormChange('topic', e.target.value)}
|
||||
className="w-full bg-stone-900 border border-stone-600 rounded-lg px-3 py-2 text-sm text-stone-100 focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500/50"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-stone-400 mb-1">Nicchia</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.nicchia}
|
||||
onChange={(e) => onEditFormChange('nicchia', e.target.value)}
|
||||
placeholder="Es. dentisti"
|
||||
className="w-full bg-stone-900 border border-stone-600 rounded-lg px-3 py-2 text-sm text-stone-100 focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500/50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-stone-400 mb-1">Note</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.note}
|
||||
onChange={(e) => onEditFormChange('note', e.target.value)}
|
||||
placeholder="Note aggiuntive..."
|
||||
className="w-full bg-stone-900 border border-stone-600 rounded-lg px-3 py-2 text-sm text-stone-100 focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-1">
|
||||
<button
|
||||
onClick={onSaveEdit}
|
||||
disabled={isSaving || editForm.topic.trim().length < 3}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-amber-500 text-stone-950 hover:bg-amber-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Check size={13} />
|
||||
{isSaving ? 'Salvataggio...' : 'Salva'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onCancelEdit}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-stone-700 text-stone-300 hover:bg-stone-600 transition-colors"
|
||||
>
|
||||
<X size={13} />
|
||||
Annulla
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Modalita' visualizzazione */
|
||||
<>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold text-stone-100 leading-snug">{item.topic}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={onStartEdit}
|
||||
title="Modifica"
|
||||
className="p-1.5 rounded-lg text-stone-500 hover:text-stone-200 hover:bg-stone-700 transition-colors"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onRequestDelete}
|
||||
title="Elimina"
|
||||
className="p-1.5 rounded-lg text-stone-500 hover:text-red-400 hover:bg-red-400/10 transition-colors"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badge nicchia e "Usato" */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{item.nicchia && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-stone-700 text-stone-300 border border-stone-600">
|
||||
{item.nicchia}
|
||||
</span>
|
||||
)}
|
||||
{item.used && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-amber-500/15 text-amber-400 border border-amber-500/30">
|
||||
<CheckCircle size={11} />
|
||||
Usato
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Note */}
|
||||
{item.note && (
|
||||
<p className="text-xs text-stone-400 leading-relaxed">{item.note}</p>
|
||||
)}
|
||||
|
||||
{/* Data relativa */}
|
||||
<p className="text-xs text-stone-600">{relativeTime(item.created_at)}</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Dialog conferma eliminazione */}
|
||||
{deleteConfirm && (
|
||||
<div className="border-t border-stone-700 pt-3 space-y-2">
|
||||
<p className="text-xs text-stone-300">
|
||||
Eliminare questa idea? L'azione e' irreversibile.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onConfirmDelete}
|
||||
disabled={isDeleting}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-red-600 text-white hover:bg-red-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{isDeleting ? 'Eliminazione...' : 'Elimina'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onCancelDelete}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-medium bg-stone-700 text-stone-300 hover:bg-stone-600 transition-colors"
|
||||
>
|
||||
Annulla
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pagina principale
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function SwipeFile() {
|
||||
// Form aggiunta
|
||||
const [newItem, setNewItem] = useState({ topic: '', nicchia: '', note: '' })
|
||||
|
||||
// Modifica inline
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
const [editForm, setEditForm] = useState({ topic: '', nicchia: '', note: '' })
|
||||
|
||||
// Conferma eliminazione
|
||||
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null)
|
||||
|
||||
// Filtro nicchia
|
||||
const [filterNicchia, setFilterNicchia] = useState<string | null>(null)
|
||||
|
||||
// API hooks
|
||||
const { data, isLoading, isError } = useSwipeItems()
|
||||
const addMutation = useAddSwipeItem()
|
||||
const updateMutation = useUpdateSwipeItem()
|
||||
const deleteMutation = useDeleteSwipeItem()
|
||||
|
||||
const items = data?.items ?? []
|
||||
|
||||
// Nicchie uniche dalle idee (per chip filtro)
|
||||
const uniqueNicchie = Array.from(
|
||||
new Set(items.map((i) => i.nicchia).filter(Boolean) as string[]),
|
||||
).sort()
|
||||
|
||||
// Lista filtrata
|
||||
const filteredItems =
|
||||
filterNicchia === null ? items : items.filter((i) => i.nicchia === filterNicchia)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handlers: form aggiunta
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const handleAddSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (newItem.topic.trim().length < 3) return
|
||||
addMutation.mutate(
|
||||
{
|
||||
topic: newItem.topic.trim(),
|
||||
nicchia: newItem.nicchia.trim() || null,
|
||||
note: newItem.note.trim() || null,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setNewItem({ topic: '', nicchia: '', note: '' })
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handlers: modifica inline
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const handleStartEdit = (item: SwipeItem) => {
|
||||
setEditingId(item.id)
|
||||
setEditForm({
|
||||
topic: item.topic,
|
||||
nicchia: item.nicchia ?? '',
|
||||
note: item.note ?? '',
|
||||
})
|
||||
// Chiudi eventuale dialog di conferma aperto su questo item
|
||||
if (deleteConfirmId === item.id) setDeleteConfirmId(null)
|
||||
}
|
||||
|
||||
const handleEditFormChange = (field: 'topic' | 'nicchia' | 'note', value: string) => {
|
||||
setEditForm((prev) => ({ ...prev, [field]: value }))
|
||||
}
|
||||
|
||||
const handleSaveEdit = (itemId: string) => {
|
||||
const update: SwipeItemUpdate = {
|
||||
topic: editForm.topic.trim() || undefined,
|
||||
nicchia: editForm.nicchia.trim() || null,
|
||||
note: editForm.note.trim() || null,
|
||||
}
|
||||
updateMutation.mutate(
|
||||
{ id: itemId, data: update },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setEditingId(null)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditingId(null)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handlers: eliminazione
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const handleRequestDelete = (itemId: string) => {
|
||||
setDeleteConfirmId(itemId)
|
||||
// Se stiamo editando questo item, chiudi la modalita' edit
|
||||
if (editingId === itemId) setEditingId(null)
|
||||
}
|
||||
|
||||
const handleConfirmDelete = (itemId: string) => {
|
||||
deleteMutation.mutate(itemId, {
|
||||
onSuccess: () => {
|
||||
setDeleteConfirmId(null)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleCancelDelete = () => {
|
||||
setDeleteConfirmId(null)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Render
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Lightbulb size={20} className="text-amber-400" />
|
||||
<h1 className="text-lg font-semibold text-stone-100">Swipe File</h1>
|
||||
</div>
|
||||
<p className="text-sm text-stone-400">
|
||||
Cattura idee e topic interessanti per riusarli nei calendari.
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0">
|
||||
<p className="text-2xl font-bold text-stone-100">{data?.total ?? 0}</p>
|
||||
<p className="text-xs text-stone-500">idee salvate</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form aggiunta rapida */}
|
||||
<div className="bg-stone-800 border border-stone-700 rounded-xl p-4">
|
||||
<h2 className="text-xs font-semibold text-stone-400 uppercase tracking-wide mb-3">
|
||||
Aggiungi idea
|
||||
</h2>
|
||||
<form onSubmit={handleAddSubmit} className="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
value={newItem.topic}
|
||||
onChange={(e) => setNewItem((prev) => ({ ...prev, topic: e.target.value }))}
|
||||
placeholder="Scrivi un'idea o topic..."
|
||||
className="w-full bg-stone-900 border border-stone-600 rounded-lg px-3 py-2.5 text-sm text-stone-100 placeholder-stone-500 focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500/50"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={newItem.nicchia}
|
||||
onChange={(e) => setNewItem((prev) => ({ ...prev, nicchia: e.target.value }))}
|
||||
placeholder="Es. dentisti"
|
||||
className="bg-stone-900 border border-stone-600 rounded-lg px-3 py-2 text-sm text-stone-100 placeholder-stone-500 focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500/50"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={newItem.note}
|
||||
onChange={(e) => setNewItem((prev) => ({ ...prev, note: e.target.value }))}
|
||||
placeholder="Note aggiuntive..."
|
||||
className="bg-stone-900 border border-stone-600 rounded-lg px-3 py-2 text-sm text-stone-100 placeholder-stone-500 focus:outline-none focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500/50"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={newItem.topic.trim().length < 3 || addMutation.isPending}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium bg-amber-500 text-stone-950 hover:bg-amber-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Plus size={16} />
|
||||
{addMutation.isPending ? 'Aggiunta...' : 'Aggiungi'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Filtro nicchia */}
|
||||
{uniqueNicchie.length > 0 && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Filter size={14} className="text-stone-500 flex-shrink-0" />
|
||||
<button
|
||||
onClick={() => setFilterNicchia(null)}
|
||||
className={[
|
||||
'px-3 py-1 rounded-full text-xs font-medium transition-colors',
|
||||
filterNicchia === null
|
||||
? 'bg-amber-500 text-stone-950'
|
||||
: 'bg-stone-800 text-stone-400 border border-stone-700 hover:border-stone-500 hover:text-stone-200',
|
||||
].join(' ')}
|
||||
>
|
||||
Tutte
|
||||
</button>
|
||||
{uniqueNicchie.map((nicchia) => (
|
||||
<button
|
||||
key={nicchia}
|
||||
onClick={() => setFilterNicchia(nicchia === filterNicchia ? null : nicchia)}
|
||||
className={[
|
||||
'px-3 py-1 rounded-full text-xs font-medium transition-colors',
|
||||
filterNicchia === nicchia
|
||||
? 'bg-amber-500 text-stone-950'
|
||||
: 'bg-stone-800 text-stone-400 border border-stone-700 hover:border-stone-500 hover:text-stone-200',
|
||||
].join(' ')}
|
||||
>
|
||||
{nicchia}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lista idee */}
|
||||
<div className="space-y-3">
|
||||
{isLoading && (
|
||||
<div className="text-center py-8 text-stone-500 text-sm">
|
||||
Caricamento idee...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isError && (
|
||||
<div className="bg-red-500/10 border border-red-500/30 rounded-xl p-4 text-sm text-red-400">
|
||||
Errore nel caricamento delle idee. Riprova.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !isError && filteredItems.length === 0 && (
|
||||
<div className="text-center py-12 space-y-3">
|
||||
<Lightbulb size={32} className="text-stone-700 mx-auto" />
|
||||
<div>
|
||||
<p className="text-stone-400 text-sm font-medium">
|
||||
{filterNicchia
|
||||
? `Nessuna idea per la nicchia "${filterNicchia}"`
|
||||
: 'Nessuna idea salvata'}
|
||||
</p>
|
||||
{!filterNicchia && (
|
||||
<p className="text-stone-600 text-xs mt-1">
|
||||
Usa il form qui sopra per aggiungere la prima idea.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredItems.map((item) => (
|
||||
<SwipeCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
isEditing={editingId === item.id}
|
||||
editForm={editForm}
|
||||
deleteConfirm={deleteConfirmId === item.id}
|
||||
onStartEdit={() => handleStartEdit(item)}
|
||||
onEditFormChange={handleEditFormChange}
|
||||
onSaveEdit={() => handleSaveEdit(item.id)}
|
||||
onCancelEdit={handleCancelEdit}
|
||||
onRequestDelete={() => handleRequestDelete(item.id)}
|
||||
onConfirmDelete={() => handleConfirmDelete(item.id)}
|
||||
onCancelDelete={handleCancelDelete}
|
||||
isSaving={updateMutation.isPending}
|
||||
isDeleting={deleteMutation.isPending}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -179,3 +179,34 @@ export interface PromptDetail {
|
||||
variables: string[]
|
||||
modified: boolean
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Swipe File
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface SwipeItem {
|
||||
id: string
|
||||
topic: string
|
||||
nicchia?: string | null
|
||||
note?: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
used: boolean
|
||||
}
|
||||
|
||||
export interface SwipeItemCreate {
|
||||
topic: string
|
||||
nicchia?: string | null
|
||||
note?: string | null
|
||||
}
|
||||
|
||||
export interface SwipeItemUpdate {
|
||||
topic?: string | null
|
||||
nicchia?: string | null
|
||||
note?: string | null
|
||||
}
|
||||
|
||||
export interface SwipeListResponse {
|
||||
items: SwipeItem[]
|
||||
total: number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user