improving home
This commit is contained in:
119
packages/frontend/src/components/CelebrityList.css
Normal file
119
packages/frontend/src/components/CelebrityList.css
Normal file
@@ -0,0 +1,119 @@
|
||||
/* packages/frontend/src/components/CelebrityList.css */
|
||||
|
||||
.celebrity-list-container {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.list-header h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.list-controls {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.celeb-link {
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
color: var(--pico-primary);
|
||||
}
|
||||
.celeb-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Menu Azioni a tendina */
|
||||
.actions-cell {
|
||||
text-align: center;
|
||||
}
|
||||
.actions-button {
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
width: auto !important;
|
||||
display: inline-flex !important;
|
||||
}
|
||||
.actions-cell details[role="list"] {
|
||||
margin: 0;
|
||||
}
|
||||
.actions-cell summary::after {
|
||||
display: none; /* Rimuove la freccia di default */
|
||||
}
|
||||
.actions-cell ul[role="listbox"] {
|
||||
text-align: left;
|
||||
}
|
||||
.actions-cell ul a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.actions-cell ul a.danger {
|
||||
color: var(--pico-color-red-500);
|
||||
}
|
||||
|
||||
|
||||
/* Paginazione */
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 1.5rem;
|
||||
padding: 0.5rem;
|
||||
background-color: var(--pico-card-background-color);
|
||||
border-radius: var(--pico-card-border-radius);
|
||||
border: 1px solid var(--pico-card-border-color);
|
||||
}
|
||||
.pagination-controls .grid {
|
||||
margin: 0;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.pagination-controls button,
|
||||
.pagination-controls select {
|
||||
margin: 0;
|
||||
}
|
||||
.pagination-controls select {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
/* Stato vuoto */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
border: 2px dashed var(--pico-muted-border-color);
|
||||
border-radius: var(--border-radius);
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.empty-state h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.empty-state p {
|
||||
color: var(--pico-secondary);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.empty-state .primary {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Allineamento verticale nella tabella */
|
||||
tbody td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -1,11 +1,27 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
// packages/frontend/src/components/CelebrityList.jsx
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { getCelebrities, deleteCelebrity } from '../services/api';
|
||||
import './CelebrityList.css'; // Importa il nuovo CSS
|
||||
|
||||
// Icone SVG semplici per le azioni
|
||||
const EditIcon = () => <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path></svg>;
|
||||
const DeleteIcon = () => <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>;
|
||||
const MoreIcon = () => <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle><circle cx="5" cy="12" r="1"></circle></svg>;
|
||||
|
||||
|
||||
function CelebrityList() {
|
||||
const [celebrities, setCelebrities] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
// Stato per la paginazione
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
fetchCelebrities();
|
||||
@@ -28,63 +44,133 @@ function CelebrityList() {
|
||||
if (window.confirm(`Sei sicuro di voler eliminare ${name}?`)) {
|
||||
try {
|
||||
await deleteCelebrity(id);
|
||||
setCelebrities(celebrities.filter((c) => c.id !== id));
|
||||
setCelebrities(prev => prev.filter((c) => c.id !== id));
|
||||
} catch (err) {
|
||||
alert(`Errore: ${err.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Memoizzazione dei dati filtrati e paginati per performance
|
||||
const filteredCelebrities = useMemo(() => {
|
||||
return celebrities.filter(c =>
|
||||
c.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}, [celebrities, searchTerm]);
|
||||
|
||||
const paginatedCelebrities = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
return filteredCelebrities.slice(startIndex, startIndex + itemsPerPage);
|
||||
}, [filteredCelebrities, currentPage, itemsPerPage]);
|
||||
|
||||
const totalPages = Math.ceil(filteredCelebrities.length / itemsPerPage);
|
||||
|
||||
const renderPagination = () => (
|
||||
<div className="pagination-controls">
|
||||
<span>
|
||||
Pagina {currentPage} di {totalPages}
|
||||
</span>
|
||||
<div className="grid">
|
||||
<button onClick={() => setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1}>
|
||||
Precedente
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages}>
|
||||
Successiva
|
||||
</button>
|
||||
</div>
|
||||
<select value={itemsPerPage} onChange={e => { setItemsPerPage(Number(e.target.value)); setCurrentPage(1); }}>
|
||||
<option value="5">5 per pagina</option>
|
||||
<option value="10">10 per pagina</option>
|
||||
<option value="20">20 per pagina</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderEmptyState = () => (
|
||||
<div className="empty-state">
|
||||
<h3>Nessun Profilo Trovato</h3>
|
||||
<p>La tua lista è vuota. Inizia aggiungendo una nuova celebrità.</p>
|
||||
<Link to="/celebrity/new" role="button" className="primary">+ Aggiungi la prima celebrità</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div className="celebrity-list-container">
|
||||
<header className="list-header">
|
||||
<h1>Catalogo Celebrità</h1>
|
||||
<Link to="/celebrity/new" role="button" style={{ marginBottom: '1rem' }}>
|
||||
+ Aggiungi
|
||||
<Link to="/celebrity/new" role="button">
|
||||
+ Aggiungi Profilo
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<div className="list-controls">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Cerca per nome..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => { setSearchTerm(e.target.value); setCurrentPage(1); }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loading && <p aria-busy="true">Caricamento in corso...</p>}
|
||||
{error && <p>Errore: {error}</p>}
|
||||
{loading && <article aria-busy="true">Caricamento in corso...</article>}
|
||||
{error && <p className="error-message">Errore: {error}</p>}
|
||||
|
||||
{!loading && (
|
||||
<figure>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Nome</th>
|
||||
<th scope="col">Seno (cm)</th>
|
||||
<th scope="col">Vita (cm)</th>
|
||||
<th scope="col">Fianchi (cm)</th>
|
||||
<th scope="col">Taglia Reggiseno</th>
|
||||
<th scope="col">Naturali?</th>
|
||||
<th scope="col">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{celebrities.map((celeb) => (
|
||||
<tr key={celeb.id}>
|
||||
<td><strong>{celeb.name}</strong></td>
|
||||
<td>{celeb.bust_cm || 'N/A'}</td>
|
||||
<td>{celeb.waist_cm || 'N/A'}</td>
|
||||
<td>{celeb.hips_cm || 'N/A'}</td>
|
||||
<td>
|
||||
{celeb.bra_band_size && celeb.bra_cup_size
|
||||
? `${celeb.bra_band_size}${celeb.bra_cup_size} (${celeb.bra_size_system})`
|
||||
: 'N/A'}
|
||||
</td>
|
||||
<td>{celeb.boobs_are_natural === null ? 'N/A' : celeb.boobs_are_natural ? 'Sì' : 'No'}</td>
|
||||
<td>
|
||||
<div role="group" style={{ margin: 0, padding: 0 }}>
|
||||
<Link to={`/celebrity/${celeb.id}`} role="button" className="outline">Modifica</Link>
|
||||
<button className="secondary" onClick={() => handleDelete(celeb.id, celeb.name)}>Elimina</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
{!loading && !error && (
|
||||
<>
|
||||
{celebrities.length === 0 ? renderEmptyState() : (
|
||||
<>
|
||||
<div className="table-responsive">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style={{ width: '50px' }}></th>
|
||||
<th scope="col">Nome</th>
|
||||
<th scope="col">Misure (S-V-F)</th>
|
||||
<th scope="col">Taglia Reggiseno</th>
|
||||
<th scope="col">Naturale?</th>
|
||||
<th scope="col" style={{ width: '80px', textAlign: 'center' }}>Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginatedCelebrities.map((celeb) => (
|
||||
<tr key={celeb.id}>
|
||||
<td>
|
||||
<img src={`https://i.pravatar.cc/50?u=${celeb.id}`} alt={celeb.name} className="avatar-image" />
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/celebrity/${celeb.id}`} className="celeb-link">
|
||||
<strong>{celeb.name}</strong>
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
{celeb.bust_cm || '?'}-{celeb.waist_cm || '?'}-{celeb.hips_cm || '?'} cm
|
||||
</td>
|
||||
<td>
|
||||
{celeb.bra_band_size && celeb.bra_cup_size
|
||||
? `${celeb.bra_band_size}${celeb.bra_cup_size} (${celeb.bra_size_system || 'N/A'})`
|
||||
: 'N/A'}
|
||||
</td>
|
||||
<td>
|
||||
{celeb.boobs_are_natural === null ? '?' : celeb.boobs_are_natural ? 'Sì' : 'No'}
|
||||
</td>
|
||||
<td className="actions-cell">
|
||||
<details role="list" dir="rtl">
|
||||
<summary aria-haspopup="listbox" role="button" className="outline actions-button"><MoreIcon/></summary>
|
||||
<ul role="listbox">
|
||||
<li><a href="#" onClick={(e) => { e.preventDefault(); navigate(`/celebrity/${celeb.id}`); }}><EditIcon/> Modifica</a></li>
|
||||
<li><a href="#" onClick={(e) => { e.preventDefault(); handleDelete(celeb.id, celeb.name); }} className="danger"><DeleteIcon/> Elimina</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{totalPages > 1 && renderPagination()}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user