CODEX CHATGPT

 

CODEX DI CHATGPT RISPONDE:

import React, { useEffect, useMemo, useState } from “react”;
import { Card, CardContent, CardHeader, CardTitle } from “@/components/ui/card”;
import { Button } from “@/components/ui/button”;
import { Input } from “@/components/ui/input”;
import { Badge } from “@/components/ui/badge”;
import { Textarea } from “@/components/ui/textarea”;
import { Label } from “@/components/ui/label”;
import { Checkbox } from “@/components/ui/checkbox”;
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from “@/components/ui/dialog”;
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from “@/components/ui/select”;
import { Slider } from “@/components/ui/slider”;
import { X, Star, Heart, HeartOff, MapPin, Search, Plus, Link as LinkIcon, Filter } from “lucide-react”;

// Utility: normalize strings (case/accents)
const norm = (s: string) => (s || “”).normalize(“NFD”).replace(/\p{Diacritic}/gu, “”).toLowerCase();

// Seed data — a compact curated sample across major Italian cities
export type Place = {
id: string;
name: string;
address: string;
city: string;
region: string;
category: “Trattoria” | “Pizzeria” | “Osteria” | “Street Food” | “Ristorante” | “Pesce” | “Gelateria”;
price: “€” | “€€” | “€€€” | “€€€€”;
rating: number; // 1-5
tags?: string[];
website?: string;
phone?: string;
notes?: string;
};

const SEED: Place[] = [
{ id: “rm1”, name: “Trattoria da Nonna Lella”, address: “Via dei Serpenti 45”, city: “Roma”, region: “Lazio”, category: “Trattoria”, price: “€€”, rating: 4.7, tags: [“cacio e pepe”, “amatriciana”], phone: “+39 06 1234 5678” },
{ id: “rm2”, name: “Pizzeria Forno Testaccio”, address: “Via Vespa 12”, city: “Roma”, region: “Lazio”, category: “Pizzeria”, price: “€”, rating: 4.5, tags: [“pizza alla pala”, “takeaway”] },
{ id: “mi1”, name: “Osteria Navigli”, address: “Alzaia Naviglio Grande 102”, city: “Milano”, region: “Lombardia”, category: “Osteria”, price: “€€€”, rating: 4.6, tags: [“risotti”, “vini naturali”], website: “https://example.com” },
{ id: “mi2”, name: “Trippa Milano”, address: “Via Giorgio Vasari 1”, city: “Milano”, region: “Lombardia”, category: “Ristorante”, price: “€€€”, rating: 4.8, tags: [“contemporaneo”, “offal”], phone: “+39 02 0000 1111” },
{ id: “na1”, name: “Pizzeria O’ Vesuvio”, address: “Via dei Tribunali 85”, city: “Napoli”, region: “Campania”, category: “Pizzeria”, price: “€”, rating: 4.9, tags: [“verace”, “margherita”] },
{ id: “na2”, name: “Pescheria Santa Lucia”, address: “Borgo Santa Lucia 18”, city: “Napoli”, region: “Campania”, category: “Pesce”, price: “€€”, rating: 4.4, tags: [“frittura”, “crudi”] },
{ id: “to1”, name: “Osteria del Quadrilatero”, address: “Via Mercanti 9”, city: “Torino”, region: “Piemonte”, category: “Osteria”, price: “€€”, rating: 4.5, tags: [“agnolotti”, “barbera”] },
{ id: “fi1”, name: “Trattoria Santo Spirito”, address: “Piazza Santo Spirito 16”, city: “Firenze”, region: “Toscana”, category: “Trattoria”, price: “€€”, rating: 4.6, tags: [“bistecca”, “ribollita”] },
{ id: “bo1”, name: “Tagliatella House”, address: “Via delle Moline 8”, city: “Bologna”, region: “Emilia-Romagna”, category: “Trattoria”, price: “€€”, rating: 4.7, tags: [“ragù”, “tortellini”] },
{ id: “pa1”, name: “Osteria dei Quattro Canti”, address: “Via Maqueda 123”, city: “Palermo”, region: “Sicilia”, category: “Osteria”, price: “€”, rating: 4.5, tags: [“sarde a beccafico”, “arancine”] },
{ id: “ct1”, name: “Friggitoria Bellini”, address: “Piazza Bellini 3”, city: “Catania”, region: “Sicilia”, category: “Street Food”, price: “€”, rating: 4.3, tags: [“cartocciata”, “cipollina”] },
{ id: “ba1”, name: “Rosticceria Barese”, address: “Via Sparano 55”, city: “Bari”, region: “Puglia”, category: “Street Food”, price: “€”, rating: 4.4, tags: [“focaccia barese”, “panzerotti”] },
{ id: “ve1”, name: “Bacaro al Ponte”, address: “Fondamenta Nove 2104”, city: “Venezia”, region: “Veneto”, category: “Osteria”, price: “€€”, rating: 4.6, tags: [“cicchetti”, “ombra”] },
{ id: “ge1”, name: “Antica Focacceria Genovese”, address: “Via San Lorenzo 24”, city: “Genova”, region: “Liguria”, category: “Street Food”, price: “€”, rating: 4.2, tags: [“focaccia”, “farinata”] },
{ id: “ve2”, name: “Trattoria alla Buranella”, address: “Burano 12”, city: “Venezia”, region: “Veneto”, category: “Pesce”, price: “€€€”, rating: 4.7, tags: [“risotto di go”, “seppie”] },
{ id: “rm3”, name: “Gelateria del Pantheon”, address: “Piazza della Rotonda 5”, city: “Roma”, region: “Lazio”, category: “Gelateria”, price: “€”, rating: 4.6, tags: [“gelato artigianale”] },
{ id: “mi3”, name: “Giargiana Panini”, address: “Viale Monza 220”, city: “Milano”, region: “Lombardia”, category: “Street Food”, price: “€”, rating: 4.1, tags: [“panini”, “vegetariano”] },
{ id: “na3”, name: “Osteria Spaccanapoli”, address: “Via San Gregorio Armeno 7”, city: “Napoli”, region: “Campania”, category: “Osteria”, price: “€€”, rating: 4.5, tags: [“genovese”, “ziti”] },
{ id: “to2”, name: “Pesce in Crocetta”, address: “Corso Alcide De Gasperi 21”, city: “Torino”, region: “Piemonte”, category: “Pesce”, price: “€€€”, rating: 4.4, tags: [“crudo”, “bistrot”] },
{ id: “fi2”, name: “Pizzeria Oltrarno”, address: “Via dei Serragli 88”, city: “Firenze”, region: “Toscana”, category: “Pizzeria”, price: “€”, rating: 4.3, tags: [“napoletana”, “lievitazione lunga”] },
{ id: “bo2”, name: “Sfoglia & Sorrisi”, address: “Via Righi 2”, city: “Bologna”, region: “Emilia-Romagna”, category: “Street Food”, price: “€”, rating: 4.0, tags: [“crescentine”, “tigelle”] },
{ id: “pa2”, name: “Trattoria Ballarò”, address: “Via Ballarò 44”, city: “Palermo”, region: “Sicilia”, category: “Trattoria”, price: “€”, rating: 4.2, tags: [“pasta con le sarde”] },
{ id: “ct2”, name: “Pizzeria Etnea”, address: “Via Etnea 240”, city: “Catania”, region: “Sicilia”, category: “Pizzeria”, price: “€”, rating: 4.5, tags: [“forno a legna”] },
{ id: “ba2”, name: “Trattoria del Porto”, address: “Lungomare 10”, city: “Bari”, region: “Puglia”, category: “Pesce”, price: “€€”, rating: 4.5, tags: [“polpo”, “frisella”] },
{ id: “ge2”, name: “Osteria del Porto Antico”, address: “Calata Cattaneo 3”, city: “Genova”, region: “Liguria”, category: “Osteria”, price: “€€”, rating: 4.3, tags: [“pesto”, “trofie”] },
];

const CITIES = Array.from(new Set(SEED.map(p => p.city))).sort();
const CATEGORIES: Place[“category”][] = [“Trattoria”, “Pizzeria”, “Osteria”, “Street Food”, “Ristorante”, “Pesce”, “Gelateria”];
const PRICES: Place[“price”][] = [“€”, “€€”, “€€€”, “€€€€”];

function Stars({ value }: { value: number }) {
const full = Math.floor(value);
const half = value – full >= 0.5;
const empty = 5 – full – (half ? 1 : 0);
return (
<div className=”flex items-center” aria-label={`Valutazione ${value.toFixed(1)} su 5`}>
{Array.from({ length: full }).map((_, i) => (
<Star key={`f-${i}`} className=”w-4 h-4 fill-current” />
))}
{half && <Star className=”w-4 h-4″ />}
{Array.from({ length: empty }).map((_, i) => (
<Star key={`e-${i}`} className=”w-4 h-4″ />
))}
<span className=”ml-2 text-sm font-medium”>{value.toFixed(1)}</span>
</div>
);
}

function useLocalStorage<T>(key: string, initial: T) {
const [state, setState] = useState<T>(() => {
try {
const raw = localStorage.getItem(key);
return raw ? (JSON.parse(raw) as T) : initial;
} catch {
return initial;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(state));
} catch {}
}, [key, state]);
return [state, setState] as const;
}

export default function AppMangiareBeneItalia() {
const [query, setQuery] = useState(“”);
const [city, setCity] = useState<string | undefined>(undefined);
const [category, setCategory] = useState<string | undefined>(undefined);
const [price, setPrice] = useState<string | undefined>(undefined);
const [minRating, setMinRating] = useState(4);
const [onlyFavs, setOnlyFavs] = useState(false);
const [places, setPlaces] = useState<Place[]>(SEED);
const [favs, setFavs] = useLocalStorage<string[]>(“mbi:favorites”, []);

const [openAdd, setOpenAdd] = useState(false);
const [draft, setDraft] = useState<Partial<Place>>({ city: “Roma”, category: “Trattoria”, price: “€€”, rating: 4.5 });

const filtered = useMemo(() => {
return places
.filter(p => !onlyFavs || favs.includes(p.id))
.filter(p => (city ? p.city === city : true))
.filter(p => (category ? p.category === category : true))
.filter(p => (price ? p.price === price : true))
.filter(p => p.rating >= minRating)
.filter(p => {
if (!query) return true;
const q = norm(query);
const hay = [p.name, p.address, p.city, p.region, p.tags?.join(” “) || “”, p.notes || “”].map(norm).join(” “);
return hay.includes(q);
})
.sort((a, b) => b.rating – a.rating);
}, [places, city, category, price, minRating, query, favs, onlyFavs]);

const toggleFav = (id: string) => {
setFavs(prev => (prev.includes(id) ? prev.filter(v => v !== id) : […prev, id]));
};

const copyShare = async () => {
const params = new URLSearchParams();
if (city) params.set(“city”, city);
if (category) params.set(“category”, category);
if (price) params.set(“price”, price);
if (query) params.set(“q”, query);
params.set(“min”, String(minRating));
const url = `${location.origin}${location.pathname}?${params.toString()}`;
try {
await navigator.clipboard.writeText(url);
alert(“Link copiato negli appunti!”);
} catch {
alert(url);
}
};

const reset = () => {
setQuery(“”);
setCity(undefined);
setCategory(undefined);
setPrice(undefined);
setMinRating(4);
setOnlyFavs(false);
};

const onAddPlace = () => {
if (!draft.name || !draft.address || !draft.city || !draft.category || !draft.price || !draft.rating) return;
const id = `${norm(draft.city)}-${Date.now()}`;
const newPlace: Place = {
id,
name: draft.name!,
address: draft.address!,
city: draft.city!,
region: draft.region || “”,
category: draft.category as Place[“category”],
price: draft.price as Place[“price”],
rating: Math.min(5, Math.max(1, Number(draft.rating))),
tags: draft.tags || [],
website: draft.website,
phone: draft.phone,
notes: draft.notes,
};
setPlaces(prev => [newPlace, …prev]);
setOpenAdd(false);
setDraft({ city: draft.city, category: draft.category, price: draft.price, rating: draft.rating });
};

// hydrate from URL
useEffect(() => {
const u = new URL(window.location.href);
const c = u.searchParams.get(“city”);
const k = u.searchParams.get(“category”);
const p = u.searchParams.get(“price”);
const q = u.searchParams.get(“q”);
const m = u.searchParams.get(“min”);
if (c) setCity(c);
if (k) setCategory(k);
if (p) setPrice(p);
if (q) setQuery(q);
if (m) setMinRating(parseInt(m));
}, []);

return (
<div className=”min-h-screen bg-gradient-to-b from-white to-slate-50″>
<header className=”sticky top-0 z-30 backdrop-blur bg-white/75 border-b”>
<div className=”max-w-6xl mx-auto px-4 py-3 flex items-center gap-2″>
<MapPin className=”w-6 h-6″ />
<h1 className=”text-xl sm:text-2xl font-bold”>Mangiare Bene in Italia</h1>
<div className=”ml-auto flex gap-2″>
<Button variant=”secondary” onClick={copyShare} className=”gap-2″><LinkIcon className=”w-4 h-4″/>Condividi filtro</Button>
<Dialog open={openAdd} onOpenChange={setOpenAdd}>
<DialogTrigger asChild>
<Button className=”gap-2″><Plus className=”w-4 h-4″/>Aggiungi posto</Button>
</DialogTrigger>
<DialogContent className=”max-w-xl”>
<DialogHeader>
<DialogTitle>Aggiungi un nuovo posto</DialogTitle>
</DialogHeader>
<div className=”grid grid-cols-1 sm:grid-cols-2 gap-3″>
<div>
<Label>Nome</Label>
<Input value={draft.name||””} onChange={e=>setDraft(d=>({…d, name:e.target.value}))} placeholder=”Es. Trattoria da Mario” />
</div>
<div>
<Label>Indirizzo</Label>
<Input value={draft.address||””} onChange={e=>setDraft(d=>({…d, address:e.target.value}))} placeholder=”Via Roma 10″ />
</div>
<div>
<Label>Città</Label>
<Select value={draft.city} onValueChange={(v)=>setDraft(d=>({…d, city:v}))}>
<SelectTrigger><SelectValue placeholder=”Seleziona città”/></SelectTrigger>
<SelectContent>
{Array.from(new Set([…CITIES, draft.city || “”]))
.filter(Boolean)
.sort()
.map(c=> (<SelectItem key={c} value={c}>{c}</SelectItem>))}
</SelectContent>
</Select>
</div>
<div>
<Label>Categoria</Label>
<Select value={draft.category as string} onValueChange={(v)=>setDraft(d=>({…d, category: v as any}))}>
<SelectTrigger><SelectValue placeholder=”Seleziona categoria”/></SelectTrigger>
<SelectContent>
{CATEGORIES.map(c=> (<SelectItem key={c} value={c}>{c}</SelectItem>))}
</SelectContent>
</Select>
</div>
<div>
<Label>Prezzo</Label>
<Select value={draft.price as string} onValueChange={(v)=>setDraft(d=>({…d, price: v as any}))}>
<SelectTrigger><SelectValue placeholder=”Fascia prezzo”/></SelectTrigger>
<SelectContent>
{PRICES.map(p=> (<SelectItem key={p} value={p}>{p}</SelectItem>))}
</SelectContent>
</Select>
</div>
<div>
<Label>Rating (1-5)</Label>
<Input type=”number” min={1} max={5} step={0.1} value={draft.rating||”” as any} onChange={e=>setDraft(d=>({…d, rating: Number(e.target.value)}))}/>
</div>
<div className=”sm:col-span-2″>
<Label>Tag (separati da virgola)</Label>
<Input value={(draft.tags||[]).join(“, “)} onChange={e=>setDraft(d=>({…d, tags: e.target.value.split(“,”).map(s=>s.trim()).filter(Boolean)}))} placeholder=”gluten free, dehors, panoramic” />
</div>
<div>
<Label>Telefono</Label>
<Input value={draft.phone||””} onChange={e=>setDraft(d=>({…d, phone:e.target.value}))} />
</div>
<div>
<Label>Sito web</Label>
<Input value={draft.website||””} onChange={e=>setDraft(d=>({…d, website:e.target.value}))} />
</div>
<div className=”sm:col-span-2″>
<Label>Note</Label>
<Textarea value={draft.notes||””} onChange={e=>setDraft(d=>({…d, notes:e.target.value}))} placeholder=”Specialità della casa, orari, prenotazione consigliata…” />
</div>
</div>
<DialogFooter>
<Button variant=”secondary” onClick={()=>setOpenAdd(false)}>Annulla</Button>
<Button onClick={onAddPlace}>Salva</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
</header>

<main className=”max-w-6xl mx-auto px-4 py-6″>
{/* Filters */}
<div className=”grid grid-cols-1 sm:grid-cols-12 gap-3 items-end”>
<div className=”sm:col-span-4″>
<Label className=”mb-1 block”>Cerca</Label>
<div className=”flex gap-2″>
<Input placeholder=”parola chiave, piatto, indirizzo…” value={query} onChange={(e)=>setQuery(e.target.value)} />
<Button variant=”outline” onClick={reset} className=”gap-2″><X className=”w-4 h-4″/>Reset</Button>
</div>
</div>
<div className=”sm:col-span-3″>
<Label className=”mb-1 block”>Città</Label>
<Select value={city} onValueChange={setCity}>
<SelectTrigger><SelectValue placeholder=”Seleziona”/></SelectTrigger>
<SelectContent>
{CITIES.map(c => (<SelectItem key={c} value={c}>{c}</SelectItem>))}
</SelectContent>
</Select>
</div>
<div className=”sm:col-span-3″>
<Label className=”mb-1 block”>Categoria</Label>
<Select value={category} onValueChange={setCategory}>
<SelectTrigger><SelectValue placeholder=”Tutte”/></SelectTrigger>
<SelectContent>
{CATEGORIES.map(c => (<SelectItem key={c} value={c}>{c}</SelectItem>))}
</SelectContent>
</Select>
</div>
<div className=”sm:col-span-2″>
<Label className=”mb-1 block”>Prezzo</Label>
<Select value={price} onValueChange={setPrice}>
<SelectTrigger><SelectValue placeholder=”Tutti”/></SelectTrigger>
<SelectContent>
{PRICES.map(p => (<SelectItem key={p} value={p}>{p}</SelectItem>))}
</SelectContent>
</Select>
</div>
<div className=”sm:col-span-6″>
<Label className=”mb-1 block”>Valutazione minima</Label>
<div className=”flex items-center gap-3″>
<div className=”flex-1″><Slider value={[minRating]} min={1} max={5} step={0.5} onValueChange={(v)=>setMinRating(v[0])} /></div>
<span className=”w-10 text-sm text-right”>{minRating.toFixed(1)}</span>
</div>
</div>
<div className=”sm:col-span-3 flex items-center gap-2″>
<Checkbox id=”favsonly” checked={onlyFavs} onCheckedChange={(v)=>setOnlyFavs(Boolean(v))} />
<Label htmlFor=”favsonly”>Solo preferiti</Label>
</div>
<div className=”sm:col-span-3 flex justify-end”>
<Button variant=”outline” className=”gap-2″><Filter className=”w-4 h-4″/>Filtri smart</Button>
</div>
</div>

{/* Empty state */}
{filtered.length === 0 && (
<div className=”text-center py-20″>
<Search className=”w-10 h-10 mx-auto” />
<p className=”mt-4 text-slate-600″>Nessun risultato. Prova ad allargare i filtri o aggiungi un nuovo posto.</p>
</div>
)}

{/* Results */}
<div className=”mt-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4″>
{filtered.map(p => (
<Card key={p.id} className=”hover:shadow-lg transition-shadow”>
<CardHeader className=”pb-2″>
<div className=”flex items-start justify-between gap-3″>
<div>
<CardTitle className=”text-lg leading-tight”>{p.name}</CardTitle>
<div className=”text-sm text-slate-600 flex items-center gap-2 mt-1″>
<MapPin className=”w-4 h-4″/>
<span>{p.address}, {p.city}</span>
</div>
</div>
<Button variant={favs.includes(p.id) ? “default” : “outline”} size=”sm” onClick={()=>toggleFav(p.id)} className=”gap-1″>
{favs.includes(p.id) ? <Heart className=”w-4 h-4″/> : <HeartOff className=”w-4 h-4″/>}
<span>{favs.includes(p.id) ? “Salvato” : “Salva”}</span>
</Button>
</div>
</CardHeader>
<CardContent className=”space-y-3″>
<div className=”flex items-center justify-between”>
<div className=”flex items-center gap-2″>
<Badge variant=”secondary”>{p.category}</Badge>
<Badge>{p.price}</Badge>
</div>
<Stars value={p.rating} />
</div>
{p.tags && p.tags.length > 0 && (
<div className=”flex flex-wrap gap-2″>
{p.tags.map(t => <Badge key={t} variant=”outline”>#{t}</Badge>)}
</div>
)}
<div className=”text-sm text-slate-700″>
{p.notes ? p.notes : “”}
</div>
<div className=”flex gap-2″>
{p.phone && <Button variant=”outline” asChild><a href={`tel:${p.phone}`}>Chiama</a></Button>}
{p.website && <Button variant=”outline” asChild><a href={p.website} target=”_blank” rel=”noreferrer”>Sito</a></Button>}
</div>
</CardContent>
</Card>
))}
</div>
</main>

<footer className=”max-w-6xl mx-auto px-4 py-8 text-center text-sm text-slate-500″>
<p>Demo offline. Puoi aggiungere i tuoi indirizzi, salvarli tra i preferiti e condividere i filtri con un link.</p>
</footer>
</div>
);
}

 

ANTEPRIMA

 

 

 

Fatto! Ti ho messo nell’area accanto un’app React pronta all’uso (“Mangiare Bene in Italia”) con:

  • Ricerca per città, categoria, prezzo e rating minimo

  • Lista di indirizzi già caricati in grandi città italiane

  • Preferiti (persistono in localStorage)

  • Condivisione dei filtri tramite link

  • Modale “Aggiungi posto” per inserire nuovi indirizzi