Rete Civica Sezione · JOBCOIN · marketplace civico Intro statica Home InfoComuni
⚠ Il marketplace live richiede una connessione internet (React + Babel da CDN). Stai navigando offline? Apri la versione statica della sezione JobCoin — funziona da disco.
`; } if (type === "impianto") { const singleCard = `
${p.name}
${"★".repeat(Math.round(p.stars))}${"☆".repeat(5-Math.round(p.stars))}
jobcon.click
${p.name}
${p.title} ${p.verified ? '' : ''}
${phone}
${email}
${p.city}
${p.services.join(" · ")}
`; html = `Impianto A4 (8 biglietti) - ${p.name}
✂ Impianto A4 — 8 biglietti da visita — ${p.name} — Ritaglia lungo le linee tratteggiate
${singleCard}${singleCard}${singleCard}${singleCard}${singleCard}${singleCard}${singleCard}${singleCard}
`; } if (type === "volantino") { html = `Volantino - ${p.name}
JOBCON.CLICK — IL SOCIAL NETWORK DEL LAVORO LOCALE
${p.name}
${p.name}
${p.title}
${"★".repeat(Math.round(p.stars))} ${p.stars}/5 (${p.reviews} recensioni) · ${p.city}, Puglia
Chi sono
${p.bio}
Servizi offerti
${p.services.map(s => '
' + s + '
').join('')}
Portfolio
${portfolioImgs.map(img => '').join('')}
Competenze
${p.skills.map(s => '' + s + '').join('')}
Telefono
${phone}
Email
${email}
Città
${p.city}
WhatsApp
Disponibile
`; } if (type === "catalogo") { html = `Catalogo Portfolio - ${p.name}
${p.name}
${p.name}
${p.title}
${"★".repeat(Math.round(p.stars))} ${p.stars}/5 · ${p.reviews} recensioni
📍 ${p.city}, Puglia
${p.bio}
JOBCON.CLICK — IL SOCIAL NETWORK DEL LAVORO LOCALE
Portfolio lavori
${cat?.name || p.category} — Una selezione dei lavori svolti
Informazioni professionali
${p.bio}
Servizi e competenze
Scopri cosa posso fare per te
${p.services.map((s, i) => '
' + s + '
Servizio professionale con esperienza pluriennale. Contattami per un preventivo gratuito.
').join('')}
Competenze chiave
${p.skills.map(s => '' + s + '').join('')}
`; } // Open in new tab for print/save-as-PDF const w = window.open("", "_blank"); if (w) { w.document.write(html); w.document.close(); } }; /* ═══════════════════════════════════════════════════ MINIMAP COMPONENT ═══════════════════════════════════════════════════ */ const MiniMap = ({ city, province }) => { const mapRef = useRef(null); const mapInstanceRef = useRef(null); const PROVINCE_COORDS = { brindisi: [40.6278, 17.9417], lecce: [40.3516, 18.1718], taranto: [40.4644, 17.2470], bari: [41.1171, 16.8719], foggia: [41.4622, 15.5446], bat: [41.2267, 16.2955] }; useEffect(() => { if (mapRef.current && !mapInstanceRef.current && typeof L !== 'undefined') { const coords = PROVINCE_COORDS[province] || [40.6278, 17.9417]; try { const map = L.map(mapRef.current, { zoomControl: false, attributionControl: false }).setView(coords, 11); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map); L.marker(coords).addTo(map).bindPopup(city || 'Puglia'); mapInstanceRef.current = map; } catch (e) { console.log("Map error:", e); } } return () => { if (mapInstanceRef.current) { try { mapInstanceRef.current.remove(); mapInstanceRef.current = null; } catch (e) {} } }; }, [city, province]); return
; }; /* ═══════════════════════════════════════════════════ REVIEWS SECTION COMPONENT ═══════════════════════════════════════════════════ */ const ReviewsSection = ({ profileId, reviews, profile, currentUser, openReg, openLogin }) => { const [showAllReviews, setShowAllReviews] = useState(false); const [newReviewStars, setNewReviewStars] = useState(0); const [newReviewText, setNewReviewText] = useState(""); const [hoverStar, setHoverStar] = useState(0); const [helpfulReviews, setHelpfulReviews] = useState({}); const isAdmin = localStorage.getItem("jobcon_admin_panel_active") === "true"; /* === Autorizzazione recensione === Un utente può recensire SOLO se: 1. È registrato e loggato (currentUser esiste) 2. Ha almeno una richiesta completata (completed: true) con questo professionista (completedWith === profileId) 3. Non ha già recensito per quella stessa richiesta */ const userCompletedRequests = useMemo(() => { if (!currentUser) return []; try { const userReqs = JSON.parse(localStorage.getItem("jobcon_user_requests") || "[]"); return userReqs.filter(r => r.completed && r.completedWith === profileId); } catch(e) { return []; } }, [currentUser, profileId]); const userAlreadyReviewedRequestIds = useMemo(() => { try { const stored = JSON.parse(localStorage.getItem("jobcon_reviews") || "{}"); const profileRevs = stored[profileId] || []; return profileRevs.filter(r => r.requestId).map(r => r.requestId); } catch(e) { return []; } }, [profileId]); const eligibleRequests = useMemo(() => { return userCompletedRequests.filter(r => !userAlreadyReviewedRequestIds.includes(r.id)); }, [userCompletedRequests, userAlreadyReviewedRequestIds]); const canReview = currentUser && eligibleRequests.length > 0; const [selectedRequestId, setSelectedRequestId] = useState(""); const allReviews = useMemo(() => { const stored = localStorage.getItem("jobcon_reviews") ? JSON.parse(localStorage.getItem("jobcon_reviews")) : {}; const reviewsList = [...(reviews || [])]; if (stored[profileId]) { reviewsList.push(...stored[profileId]); } return reviewsList.sort((a, b) => new Date(b.date) - new Date(a.date)); }, [profileId, reviews]); const stats = useMemo(() => { if (allReviews.length === 0) return { avg: 0, total: 0, distribution: {} }; let sum = 0, dist = { 5: 0, 4: 0, 3: 0, 2: 0, 1: 0 }; allReviews.forEach(r => { sum += r.stars; dist[r.stars]++; }); return { avg: (sum / allReviews.length).toFixed(1), total: allReviews.length, distribution: dist }; }, [allReviews]); const getLabelForRating = (rating) => { if (rating >= 4.5) return "Eccellente"; if (rating >= 4.0) return "Molto buono"; if (rating >= 3.0) return "Buono"; return "Sufficiente"; }; const handleSubmitReview = () => { if (!canReview) return; if (newReviewStars < 3 || newReviewStars > 5) { alert("La valutazione deve essere tra 3 e 5 stelle"); return; } if (!newReviewText.trim() || newReviewText.length === 0) { alert("Scrivi una recensione prima di pubblicare"); return; } if (newReviewText.length > 500) { alert("La recensione non può superare 500 caratteri"); return; } const reqUsed = selectedRequestId ? eligibleRequests.find(r => r.id === selectedRequestId) : eligibleRequests[0]; if (!reqUsed) return; const newReview = { id: "rev_" + Date.now(), author: currentUser.name || currentUser.nome || currentUser.email, avatar: currentUser.avatar || ("https://randomuser.me/api/portraits/" + (currentUser.gender === "F" ? "women" : "men") + "/1.jpg"), stars: newReviewStars, text: newReviewText, date: new Date().toISOString().split('T')[0], verified: true, jobType: reqUsed.title || reqUsed.category || "Servizio", requestId: reqUsed.id, helpful: 0 }; const stored = localStorage.getItem("jobcon_reviews") ? JSON.parse(localStorage.getItem("jobcon_reviews")) : {}; if (!stored[profileId]) stored[profileId] = []; stored[profileId].push(newReview); localStorage.setItem("jobcon_reviews", JSON.stringify(stored)); setNewReviewStars(0); setNewReviewText(""); setSelectedRequestId(""); alert("Grazie! La tua recensione verificata è stata pubblicata."); }; const handleDeleteReview = (reviewId) => { if (!isAdmin) return; const stored = localStorage.getItem("jobcon_reviews") ? JSON.parse(localStorage.getItem("jobcon_reviews")) : {}; if (stored[profileId]) { stored[profileId] = stored[profileId].filter(r => r.id !== reviewId); localStorage.setItem("jobcon_reviews", JSON.stringify(stored)); window.location.reload(); } }; const handleHelpful = (reviewId) => { setHelpfulReviews(prev => ({ ...prev, [reviewId]: !prev[reviewId] })); }; const displayedReviews = showAllReviews ? allReviews : allReviews.slice(0, 3); return (

Recensioni

{allReviews.length > 0 ? ( <> {/* Aggregate rating display (Booking.com style) */}
{stats.avg}
{getLabelForRating(parseFloat(stats.avg))}
{stats.total} {stats.total === 1 ? "valutazione" : "valutazioni"}
{/* Star distribution bars */}
{[5, 4, 3, 2, 1].map(stars => { const count = stats.distribution[stars] || 0; const percent = stats.total > 0 ? Math.round((count / stats.total) * 100) : 0; return (
{stars}★
{percent}%
); })}
{/* Individual review cards */}
{displayedReviews.map((review, idx) => (
{review.author}
{review.author}
{[1, 2, 3, 4, 5].map(s => ( ))}
{review.verified && Verificato} {review.jobType && {review.jobType}}
{review.date}

{review.text}

{isAdmin && ( )}
))}
{!showAllReviews && allReviews.length > 3 && ( )} ) : (

Nessuna recensione ancora. Sii il primo a valutare questo professionista!

)} {/* Review form — solo per utenti autorizzati */}
{!currentUser ? ( /* Utente non loggato */
🔒

Vuoi lasciare una recensione?

Solo gli utenti registrati che hanno effettivamente ricevuto un servizio possono recensire un professionista. Accedi o registrati per iniziare.

) : !canReview ? ( /* Utente loggato ma senza richieste completate con questo professionista */
📋

Come funzionano le recensioni

Per garantire l'autenticità, puoi recensire questo professionista solo dopo aver completato un lavoro con lui. Il flusso è: pubblica una richiesta → il professionista completa il lavoro → chiudi la richiesta dalla sezione Richieste → lascia la tua recensione verificata.

{userCompletedRequests.length > 0 && userAlreadyReviewedRequestIds.length > 0 && (

Hai già recensito tutti i lavori completati con questo professionista.

)}
) : ( /* Utente autorizzato: ha richieste completate non ancora recensite */

Lascia una recensione verificata

Recensione verificata — hai completato un lavoro con questo professionista

{eligibleRequests.length > 1 && (
)}
{[1, 2, 3, 4, 5].map(s => ( ))}
{newReviewStars > 0 && newReviewStars < 3 &&
La valutazione deve essere almeno 3 stelle
}