440 lines
18 KiB
TypeScript
440 lines
18 KiB
TypeScript
|
|
import type { Metadata } from "next";
|
|||
|
|
import { notFound } from "next/navigation";
|
|||
|
|
import { getTranslations } from "next-intl/server";
|
|||
|
|
import { Navbar } from "@/components/layout/navbar";
|
|||
|
|
import { Footer } from "@/components/layout/footer";
|
|||
|
|
import { CafeJsonLd } from "@/components/seo/cafe-json-ld";
|
|||
|
|
import { getCafe, getCafeMenu, getCafeReviews, getAllCafeSlugs, discoverCafes } from "@/lib/api";
|
|||
|
|
import {
|
|||
|
|
MapPin, Phone, Clock, Star, BadgeCheck, Instagram,
|
|||
|
|
Globe, ChevronLeft, Wifi, Coffee, Users
|
|||
|
|
} from "lucide-react";
|
|||
|
|
import { getDayLabel, PRICE_TIER_LABELS, NOISE_LABELS, formatRating, cn } from "@/lib/utils";
|
|||
|
|
import { CafeCard } from "@/components/cafe/cafe-card";
|
|||
|
|
|
|||
|
|
const BASE = process.env.NEXT_PUBLIC_SITE_URL ?? "https://find.meezi.ir";
|
|||
|
|
|
|||
|
|
export async function generateStaticParams() {
|
|||
|
|
const slugs = await getAllCafeSlugs();
|
|||
|
|
const locales = ["fa", "en"];
|
|||
|
|
return locales.flatMap((locale) => slugs.map((slug) => ({ locale, slug })));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function generateMetadata({
|
|||
|
|
params,
|
|||
|
|
}: {
|
|||
|
|
params: Promise<{ locale: string; slug: string }>;
|
|||
|
|
}): Promise<Metadata> {
|
|||
|
|
const { locale, slug } = await params;
|
|||
|
|
const cafe = await getCafe(slug);
|
|||
|
|
if (!cafe) return { title: "Cafe not found" };
|
|||
|
|
|
|||
|
|
const name = locale === "en" && cafe.nameEn ? cafe.nameEn : cafe.name;
|
|||
|
|
const description = cafe.description
|
|||
|
|
?? (locale === "fa"
|
|||
|
|
? `${name} در ${cafe.city ?? "ایران"} — امتیاز ${formatRating(cafe.averageRating)} از ${cafe.reviewCount} نظر`
|
|||
|
|
: `${name} in ${cafe.city ?? "Iran"} — rated ${formatRating(cafe.averageRating)} from ${cafe.reviewCount} reviews`);
|
|||
|
|
|
|||
|
|
const ogImage = cafe.coverImageUrl
|
|||
|
|
?? `${BASE}/api/og?t=${encodeURIComponent(name)}&s=${encodeURIComponent(description)}`;
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
title: name,
|
|||
|
|
description,
|
|||
|
|
openGraph: {
|
|||
|
|
type: "website",
|
|||
|
|
title: name,
|
|||
|
|
description,
|
|||
|
|
images: [{ url: ogImage, width: 1200, height: 630, alt: name }],
|
|||
|
|
locale: locale === "fa" ? "fa_IR" : "en_US",
|
|||
|
|
},
|
|||
|
|
twitter: { card: "summary_large_image", title: name, description, images: [ogImage] },
|
|||
|
|
alternates: {
|
|||
|
|
canonical: `${BASE}/${locale}/cafe/${slug}`,
|
|||
|
|
languages: {
|
|||
|
|
fa: `${BASE}/fa/cafe/${slug}`,
|
|||
|
|
en: `${BASE}/en/cafe/${slug}`,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const DAY_ORDER = ["sat", "sun", "mon", "tue", "wed", "thu", "fri"] as const;
|
|||
|
|
|
|||
|
|
export default async function CafePage({
|
|||
|
|
params,
|
|||
|
|
}: {
|
|||
|
|
params: Promise<{ locale: string; slug: string }>;
|
|||
|
|
}) {
|
|||
|
|
const { locale, slug } = await params;
|
|||
|
|
const t = await getTranslations({ locale, namespace: "cafe" });
|
|||
|
|
const isFa = locale === "fa";
|
|||
|
|
|
|||
|
|
const [cafe, menu, reviews] = await Promise.all([
|
|||
|
|
getCafe(slug),
|
|||
|
|
getCafeMenu(slug),
|
|||
|
|
getCafeReviews(slug),
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
if (!cafe) notFound();
|
|||
|
|
|
|||
|
|
const name = isFa ? cafe.name : (cafe.nameEn ?? cafe.name);
|
|||
|
|
const profile = cafe.discoverProfile;
|
|||
|
|
const priceTier = profile.priceTier;
|
|||
|
|
|
|||
|
|
// Similar cafes
|
|||
|
|
const similar = cafe.city
|
|||
|
|
? (await discoverCafes({ city: cafe.city, sort: "rating" }))
|
|||
|
|
.filter((c) => c.slug !== slug)
|
|||
|
|
.slice(0, 4)
|
|||
|
|
: [];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<>
|
|||
|
|
<CafeJsonLd cafe={cafe} locale={locale} baseUrl={BASE} />
|
|||
|
|
<Navbar />
|
|||
|
|
|
|||
|
|
<main className="pb-16">
|
|||
|
|
{/* Hero cover */}
|
|||
|
|
<div className="relative h-56 overflow-hidden bg-gray-200 sm:h-80">
|
|||
|
|
{cafe.coverImageUrl ? (
|
|||
|
|
<img
|
|||
|
|
src={cafe.coverImageUrl}
|
|||
|
|
alt={name}
|
|||
|
|
className="h-full w-full object-cover"
|
|||
|
|
priority-fetch="high"
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<div className="flex h-full items-center justify-center bg-gradient-to-br from-brand-100 to-brand-200">
|
|||
|
|
<Coffee className="h-16 w-16 text-brand-300" />
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
{/* Gradient overlay */}
|
|||
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
|
|||
|
|
|
|||
|
|
{/* Back link */}
|
|||
|
|
<a
|
|||
|
|
href={`/${locale}/search`}
|
|||
|
|
className="absolute start-4 top-4 flex items-center gap-1 rounded-full bg-black/30 px-3 py-1.5 text-xs font-medium text-white backdrop-blur-sm transition hover:bg-black/50"
|
|||
|
|
>
|
|||
|
|
{isFa ? <ChevronLeft className="h-3.5 w-3.5 rotate-180" /> : <ChevronLeft className="h-3.5 w-3.5" />}
|
|||
|
|
{t("backToSearch")}
|
|||
|
|
</a>
|
|||
|
|
|
|||
|
|
{/* Open badge */}
|
|||
|
|
<div className={cn(
|
|||
|
|
"absolute end-4 top-4 rounded-full px-3 py-1 text-xs font-semibold",
|
|||
|
|
cafe.isOpenNow ? "bg-emerald-500 text-white" : "bg-gray-800/70 text-white"
|
|||
|
|
)}>
|
|||
|
|
{cafe.isOpenNow ? t("openNow") : t("closedNow")}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Name / city overlay */}
|
|||
|
|
<div className="absolute bottom-4 start-4 end-4">
|
|||
|
|
<div className="flex items-end gap-3">
|
|||
|
|
{cafe.logoUrl && (
|
|||
|
|
<img
|
|||
|
|
src={cafe.logoUrl}
|
|||
|
|
alt=""
|
|||
|
|
className="h-14 w-14 shrink-0 rounded-2xl border-2 border-white bg-white object-cover shadow-lg"
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
<div className="min-w-0">
|
|||
|
|
<h1 className="flex items-center gap-2 text-xl font-extrabold text-white sm:text-2xl">
|
|||
|
|
{name}
|
|||
|
|
{cafe.isVerified && (
|
|||
|
|
<BadgeCheck className="h-5 w-5 text-emerald-400" aria-label={t("verified")} />
|
|||
|
|
)}
|
|||
|
|
</h1>
|
|||
|
|
{cafe.city && (
|
|||
|
|
<p className="mt-0.5 flex items-center gap-1 text-sm text-white/80">
|
|||
|
|
<MapPin className="h-3.5 w-3.5" />
|
|||
|
|
{cafe.city}
|
|||
|
|
{cafe.address && ` — ${cafe.address}`}
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="mx-auto max-w-5xl px-4 sm:px-6">
|
|||
|
|
{/* Quick stats row */}
|
|||
|
|
<div className="mt-6 flex flex-wrap items-center gap-4">
|
|||
|
|
{cafe.reviewCount > 0 && (
|
|||
|
|
<div className="flex items-center gap-1.5">
|
|||
|
|
<Star className="h-4 w-4 fill-amber-400 text-amber-400" />
|
|||
|
|
<span className="text-lg font-bold">{formatRating(cafe.averageRating)}</span>
|
|||
|
|
<span className="text-sm text-gray-400">({cafe.reviewCount} {t("reviews")})</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
{priceTier && (
|
|||
|
|
<span className="rounded-full border border-gray-200 px-3 py-0.5 text-sm text-gray-600">
|
|||
|
|
{PRICE_TIER_LABELS[priceTier]?.[isFa ? "fa" : "en"] ?? priceTier}
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
{profile.noiseLevel && (
|
|||
|
|
<span className="rounded-full border border-gray-200 px-3 py-0.5 text-sm text-gray-600">
|
|||
|
|
{NOISE_LABELS[profile.noiseLevel]?.[isFa ? "fa" : "en"] ?? profile.noiseLevel}
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|||
|
|
{/* Main column */}
|
|||
|
|
<div className="lg:col-span-2 space-y-6">
|
|||
|
|
|
|||
|
|
{/* Description */}
|
|||
|
|
{cafe.description && (
|
|||
|
|
<section className="rounded-2xl border border-gray-100 bg-white p-5">
|
|||
|
|
<h2 className="mb-3 text-sm font-semibold text-gray-900">{t("about")}</h2>
|
|||
|
|
<p className="text-sm leading-relaxed text-gray-600">{cafe.description}</p>
|
|||
|
|
</section>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Attributes */}
|
|||
|
|
{(profile.themes.length > 0 || profile.vibes.length > 0 || profile.occasions.length > 0 || profile.spaceFeatures.length > 0) && (
|
|||
|
|
<section className="rounded-2xl border border-gray-100 bg-white p-5">
|
|||
|
|
<h2 className="mb-4 text-sm font-semibold text-gray-900">{t("features")}</h2>
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
{profile.themes.length > 0 && (
|
|||
|
|
<TagRow label={t("themes")} tags={profile.themes} color="brand" />
|
|||
|
|
)}
|
|||
|
|
{profile.vibes.length > 0 && (
|
|||
|
|
<TagRow label={t("vibes")} tags={profile.vibes} color="purple" />
|
|||
|
|
)}
|
|||
|
|
{profile.occasions.length > 0 && (
|
|||
|
|
<TagRow label={t("occasions")} tags={profile.occasions} color="amber" />
|
|||
|
|
)}
|
|||
|
|
{profile.spaceFeatures.length > 0 && (
|
|||
|
|
<TagRow label={t("spaceFeatures")} tags={profile.spaceFeatures} color="gray" />
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Gallery */}
|
|||
|
|
{cafe.galleryUrls.length > 0 && (
|
|||
|
|
<section className="rounded-2xl border border-gray-100 bg-white p-5">
|
|||
|
|
<h2 className="mb-3 text-sm font-semibold text-gray-900">{t("gallery")}</h2>
|
|||
|
|
<div className="grid grid-cols-3 gap-2 sm:grid-cols-4">
|
|||
|
|
{cafe.galleryUrls.map((url, i) => (
|
|||
|
|
<img
|
|||
|
|
key={i}
|
|||
|
|
src={url}
|
|||
|
|
alt={`${name} — ${i + 1}`}
|
|||
|
|
className="aspect-square w-full rounded-xl object-cover"
|
|||
|
|
loading="lazy"
|
|||
|
|
/>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Menu preview */}
|
|||
|
|
{menu && menu.categories.length > 0 && (
|
|||
|
|
<section className="rounded-2xl border border-gray-100 bg-white p-5">
|
|||
|
|
<h2 className="mb-4 text-sm font-semibold text-gray-900">{t("menu")}</h2>
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
{menu.categories.slice(0, 3).map((cat) => (
|
|||
|
|
<div key={cat.id}>
|
|||
|
|
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-gray-400">
|
|||
|
|
{isFa ? cat.name : (cat.nameEn ?? cat.name)}
|
|||
|
|
</p>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{cat.items.slice(0, 4).map((item) => (
|
|||
|
|
<div key={item.id} className="flex items-center justify-between gap-3">
|
|||
|
|
<div className="flex min-w-0 items-center gap-3">
|
|||
|
|
{item.imageUrl && (
|
|||
|
|
<img src={item.imageUrl} alt="" className="h-10 w-10 shrink-0 rounded-lg object-cover" />
|
|||
|
|
)}
|
|||
|
|
<div className="min-w-0">
|
|||
|
|
<p className="truncate text-sm font-medium text-gray-800">
|
|||
|
|
{isFa ? item.name : (item.nameEn ?? item.name)}
|
|||
|
|
</p>
|
|||
|
|
{item.description && (
|
|||
|
|
<p className="truncate text-xs text-gray-400">{item.description}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<span className="shrink-0 text-sm font-semibold text-brand-700">
|
|||
|
|
{new Intl.NumberFormat(isFa ? "fa-IR" : "en-US").format(item.price)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
<a
|
|||
|
|
href={`https://app.meezi.ir/m/${cafe.slug}`}
|
|||
|
|
target="_blank"
|
|||
|
|
rel="noopener"
|
|||
|
|
className="mt-4 flex w-full items-center justify-center gap-2 rounded-xl border border-brand-200 py-2.5 text-sm font-semibold text-brand-700 transition hover:bg-brand-50"
|
|||
|
|
>
|
|||
|
|
{t("viewMenu")}
|
|||
|
|
</a>
|
|||
|
|
</section>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Reviews */}
|
|||
|
|
<section className="rounded-2xl border border-gray-100 bg-white p-5">
|
|||
|
|
<h2 className="mb-4 text-sm font-semibold text-gray-900">{t("reviewsTab")}</h2>
|
|||
|
|
{reviews.length === 0 ? (
|
|||
|
|
<p className="text-sm text-gray-400">{t("noReviews")}</p>
|
|||
|
|
) : (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
{reviews.map((r) => (
|
|||
|
|
<div key={r.id} className="border-b border-gray-50 pb-4 last:border-0">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<p className="text-sm font-semibold text-gray-900">{r.authorName}</p>
|
|||
|
|
<div className="flex items-center gap-0.5">
|
|||
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
|||
|
|
<Star
|
|||
|
|
key={i}
|
|||
|
|
className={cn("h-3 w-3", i < r.rating ? "fill-amber-400 text-amber-400" : "text-gray-200")}
|
|||
|
|
/>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
{r.comment && (
|
|||
|
|
<p className="mt-1 text-sm leading-relaxed text-gray-600">{r.comment}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</section>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Sidebar */}
|
|||
|
|
<aside className="space-y-4">
|
|||
|
|
{/* Actions */}
|
|||
|
|
<div className="rounded-2xl border border-gray-100 bg-white p-5 space-y-2.5">
|
|||
|
|
<a
|
|||
|
|
href={`https://app.meezi.ir/m/${cafe.slug}`}
|
|||
|
|
target="_blank"
|
|||
|
|
rel="noopener"
|
|||
|
|
className="flex w-full items-center justify-center gap-2 rounded-xl bg-brand-700 py-2.5 text-sm font-semibold text-white transition hover:bg-brand-800"
|
|||
|
|
>
|
|||
|
|
<Coffee className="h-4 w-4" />
|
|||
|
|
{t("viewMenu")}
|
|||
|
|
</a>
|
|||
|
|
|
|||
|
|
{cafe.phone && (
|
|||
|
|
<a
|
|||
|
|
href={`tel:${cafe.phone}`}
|
|||
|
|
className="flex w-full items-center justify-center gap-2 rounded-xl border border-gray-200 py-2.5 text-sm font-medium text-gray-700 transition hover:bg-gray-50"
|
|||
|
|
>
|
|||
|
|
<Phone className="h-4 w-4" />
|
|||
|
|
{t("phone")}
|
|||
|
|
</a>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{cafe.address && (
|
|||
|
|
<a
|
|||
|
|
href={`https://maps.google.com/?q=${encodeURIComponent(`${cafe.name} ${cafe.address}`)}`}
|
|||
|
|
target="_blank"
|
|||
|
|
rel="noopener"
|
|||
|
|
className="flex w-full items-center justify-center gap-2 rounded-xl border border-gray-200 py-2.5 text-sm font-medium text-gray-700 transition hover:bg-gray-50"
|
|||
|
|
>
|
|||
|
|
<MapPin className="h-4 w-4" />
|
|||
|
|
{t("directions")}
|
|||
|
|
</a>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{cafe.instagramHandle && (
|
|||
|
|
<a
|
|||
|
|
href={`https://instagram.com/${cafe.instagramHandle}`}
|
|||
|
|
target="_blank"
|
|||
|
|
rel="noopener"
|
|||
|
|
className="flex w-full items-center justify-center gap-2 rounded-xl border border-gray-200 py-2.5 text-sm font-medium text-gray-700 transition hover:bg-gray-50"
|
|||
|
|
>
|
|||
|
|
<Instagram className="h-4 w-4" />
|
|||
|
|
@{cafe.instagramHandle}
|
|||
|
|
</a>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Working hours */}
|
|||
|
|
{cafe.workingHours && (
|
|||
|
|
<div className="rounded-2xl border border-gray-100 bg-white p-5">
|
|||
|
|
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900">
|
|||
|
|
<Clock className="h-4 w-4 text-brand-600" />
|
|||
|
|
{t("workingHours")}
|
|||
|
|
</h3>
|
|||
|
|
<div className="space-y-1.5">
|
|||
|
|
{DAY_ORDER.map((day) => {
|
|||
|
|
const d = cafe.workingHours![day];
|
|||
|
|
if (!d) return null;
|
|||
|
|
return (
|
|||
|
|
<div key={day} className="flex items-center justify-between text-xs">
|
|||
|
|
<span className="text-gray-500">{getDayLabel(day, locale)}</span>
|
|||
|
|
<span className={d.isOpen ? "font-medium text-gray-900" : "text-gray-400"}>
|
|||
|
|
{d.isOpen && d.open && d.close ? `${d.open} – ${d.close}` : (isFa ? "تعطیل" : "Closed")}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Badges */}
|
|||
|
|
{cafe.badges.length > 0 && (
|
|||
|
|
<div className="rounded-2xl border border-gray-100 bg-white p-5">
|
|||
|
|
<div className="flex flex-wrap gap-2">
|
|||
|
|
{cafe.badges.map((b) => (
|
|||
|
|
<span
|
|||
|
|
key={b.key}
|
|||
|
|
className="flex items-center gap-1 rounded-full bg-brand-50 px-3 py-1 text-xs font-medium text-brand-700"
|
|||
|
|
>
|
|||
|
|
<span>{b.icon}</span>
|
|||
|
|
{b.label}
|
|||
|
|
</span>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</aside>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Similar cafes */}
|
|||
|
|
{similar.length > 0 && (
|
|||
|
|
<section className="mt-10">
|
|||
|
|
<h2 className="mb-5 text-lg font-bold text-gray-900">{t("similarCafes")}</h2>
|
|||
|
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
|||
|
|
{similar.map((c) => (
|
|||
|
|
<CafeCard key={c.id} cafe={c} locale={locale} href={`/${locale}/cafe/${c.slug}`} />
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</main>
|
|||
|
|
<Footer />
|
|||
|
|
</>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function TagRow({ label, tags, color }: { label: string; tags: string[]; color: string }) {
|
|||
|
|
const colorMap: Record<string, string> = {
|
|||
|
|
brand: "bg-brand-50 text-brand-700",
|
|||
|
|
purple: "bg-purple-50 text-purple-700",
|
|||
|
|
amber: "bg-amber-50 text-amber-700",
|
|||
|
|
gray: "bg-gray-100 text-gray-600",
|
|||
|
|
};
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
<p className="mb-1.5 text-xs font-medium text-gray-400">{label}</p>
|
|||
|
|
<div className="flex flex-wrap gap-1.5">
|
|||
|
|
{tags.map((tag) => (
|
|||
|
|
<span key={tag} className={cn("rounded-full px-2.5 py-0.5 text-xs font-medium", colorMap[color] ?? colorMap.gray)}>
|
|||
|
|
{tag}
|
|||
|
|
</span>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|