456 lines
14 KiB
TypeScript
456 lines
14 KiB
TypeScript
"use client";
|
||
|
||
import Image from "next/image";
|
||
import Link from "next/link";
|
||
import { useState } from "react";
|
||
import type { BusinessDetail, BusinessTabId } from "@/data/business";
|
||
import { businessTabs } from "@/data/business";
|
||
import styles from "./BusinessDetail.module.css";
|
||
|
||
function Stars({ rating }: { rating: string }) {
|
||
return (
|
||
<div className={styles.stars} aria-label={`${rating} out of 5`}>
|
||
{Array.from({ length: 5 }, (_, i) => (
|
||
<span
|
||
key={i}
|
||
className={i < 4 ? styles.starFilled : styles.starEmpty}
|
||
aria-hidden
|
||
>
|
||
★
|
||
</span>
|
||
))}
|
||
<span className={styles.rating}>{rating}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function CategoryPills({
|
||
items,
|
||
active,
|
||
onChange,
|
||
}: {
|
||
items: string[];
|
||
active: string;
|
||
onChange: (value: string) => void;
|
||
}) {
|
||
return (
|
||
<div className={styles.pills}>
|
||
{items.map((item) => (
|
||
<button
|
||
key={item}
|
||
type="button"
|
||
className={`filter-chip${item === active ? " is-active" : ""}`}
|
||
aria-pressed={item === active}
|
||
onClick={() => onChange(item)}
|
||
>
|
||
{item}
|
||
</button>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function FiltersBar({
|
||
fields,
|
||
tags,
|
||
}: {
|
||
fields: { label: string; value: string }[];
|
||
tags: { label: string; value: string }[];
|
||
}) {
|
||
return (
|
||
<>
|
||
<div className={styles.filtersBar}>
|
||
<div className={styles.filtersLabel}>
|
||
<img src="/images/icons/search.svg" alt="" width={22} height={22} />
|
||
<div>
|
||
<strong>Filters</strong>
|
||
<span>A better focus</span>
|
||
</div>
|
||
</div>
|
||
<div className={styles.filters}>
|
||
{fields.map((field) => (
|
||
<label key={field.label} className={styles.filterField}>
|
||
<span>{field.label}</span>
|
||
<input defaultValue={field.value} readOnly />
|
||
</label>
|
||
))}
|
||
<button className={`filter-chip ${styles.filterBtn}`} type="button">
|
||
Filter
|
||
<img src="/images/icons/chevron.svg" alt="" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{tags.length ? (
|
||
<div className={styles.activeTags}>
|
||
{tags.map((tag) => (
|
||
<span className={`filter-chip ${styles.activeTag}`} key={tag.label}>
|
||
<em>{tag.label}:</em> {tag.value}
|
||
<button type="button" aria-label={`Remove ${tag.label}`}>
|
||
×
|
||
</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
) : null}
|
||
</>
|
||
);
|
||
}
|
||
|
||
export default function BusinessDetailView({
|
||
business,
|
||
}: {
|
||
business: BusinessDetail;
|
||
}) {
|
||
const [tab, setTab] = useState<BusinessTabId>("products");
|
||
const [productCategory, setProductCategory] = useState("All");
|
||
const [blogCategory, setBlogCategory] = useState("All");
|
||
const [portfolioCategory, setPortfolioCategory] = useState("All");
|
||
const [hoveredPortfolio, setHoveredPortfolio] = useState<string | null>(
|
||
business.portfolios[2]?.id ?? null,
|
||
);
|
||
|
||
const products =
|
||
productCategory === "All"
|
||
? business.products
|
||
: business.products.filter((p) => p.category === productCategory);
|
||
|
||
return (
|
||
<div className={styles.root}>
|
||
<section className={styles.bannerWrap}>
|
||
<div className={styles.banner}>
|
||
<h1 className={styles.bannerTitle}>{business.name}</h1>
|
||
<nav className={styles.crumbs} aria-label="Breadcrumb">
|
||
{business.breadcrumbs.map((crumb, index) => (
|
||
<span key={`${crumb}-${index}`}>
|
||
{index > 0 ? (
|
||
<img
|
||
className={styles.crumbChevron}
|
||
src="/images/icons/chevron.svg"
|
||
alt=""
|
||
/>
|
||
) : null}
|
||
<span
|
||
className={
|
||
index === business.breadcrumbs.length - 1
|
||
? styles.crumbActive
|
||
: undefined
|
||
}
|
||
>
|
||
{crumb}
|
||
</span>
|
||
</span>
|
||
))}
|
||
</nav>
|
||
</div>
|
||
</section>
|
||
|
||
<section className={styles.hero}>
|
||
<Image
|
||
src={business.hero}
|
||
alt=""
|
||
fill
|
||
priority
|
||
className={styles.heroImg}
|
||
sizes="100vw"
|
||
/>
|
||
</section>
|
||
|
||
<section className={styles.profile}>
|
||
<div className={styles.profileBar}>
|
||
<div className={styles.identity}>
|
||
<img
|
||
className={styles.logo}
|
||
src={business.logo}
|
||
alt=""
|
||
width={75}
|
||
height={75}
|
||
/>
|
||
<div>
|
||
<h2 className={styles.name}>{business.name}</h2>
|
||
<p className={styles.tagline}>{business.tagline}</p>
|
||
</div>
|
||
</div>
|
||
<div className={styles.stats}>
|
||
<div className={styles.stat}>
|
||
<span className={styles.statLabel}>since</span>
|
||
<strong className={styles.statValueSm}>{business.since}</strong>
|
||
</div>
|
||
<div className={styles.stat}>
|
||
<span className={styles.statLabel}>province</span>
|
||
<strong className={styles.statValueSm}>{business.province}</strong>
|
||
</div>
|
||
<div className={styles.statWide}>
|
||
<strong className={styles.statValueLg}>
|
||
{business.activeProducts}
|
||
</strong>
|
||
<span className={styles.statLabelStack}>
|
||
active
|
||
<br />
|
||
product
|
||
</span>
|
||
</div>
|
||
<div className={styles.stat}>
|
||
<span className={styles.statLabel}>Telephone</span>
|
||
<strong className={styles.statValueSm}>{business.telephone}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p className={styles.description}>{business.description}</p>
|
||
</section>
|
||
|
||
<nav className={styles.tabs} aria-label="Business sections">
|
||
{businessTabs.map((item) => (
|
||
<button
|
||
key={item.id}
|
||
type="button"
|
||
className={tab === item.id ? styles.tabActive : styles.tab}
|
||
onClick={() => setTab(item.id)}
|
||
>
|
||
{item.label}
|
||
</button>
|
||
))}
|
||
</nav>
|
||
|
||
<section className={styles.panel} aria-live="polite">
|
||
{tab === "products" ? (
|
||
<>
|
||
<CategoryPills
|
||
items={business.productCategories}
|
||
active={productCategory}
|
||
onChange={setProductCategory}
|
||
/>
|
||
<FiltersBar
|
||
fields={[{ label: "Product name", value: "gold" }]}
|
||
tags={[{ label: "Name", value: "gold" }]}
|
||
/>
|
||
<div className={styles.productGrid}>
|
||
{products.map((product) => (
|
||
<article key={product.id} className={styles.productCard}>
|
||
<div className={styles.productMedia}>
|
||
<Image
|
||
src={product.image}
|
||
alt=""
|
||
fill
|
||
className={styles.productImg}
|
||
sizes="(max-width: 720px) 50vw, 292px"
|
||
/>
|
||
</div>
|
||
<div className={styles.productBody}>
|
||
<h3>{product.title}</h3>
|
||
<Stars rating={product.rating} />
|
||
<div className={styles.productMeta}>
|
||
<span className={styles.chip}>
|
||
<strong>{product.minOrder}</strong>
|
||
<em>
|
||
pcs, min
|
||
<br />
|
||
order
|
||
</em>
|
||
</span>
|
||
<span className={styles.chip}>
|
||
<strong>{product.shipsIn}</strong>
|
||
<em>
|
||
days
|
||
<br />
|
||
to ship
|
||
</em>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
))}
|
||
</div>
|
||
</>
|
||
) : null}
|
||
|
||
{tab === "contact" ? (
|
||
<div className={styles.contact}>
|
||
<div className={styles.contactRows}>
|
||
<div className={styles.contactRow}>
|
||
<span className={styles.contactIcon} aria-hidden>
|
||
⌖
|
||
</span>
|
||
<div>
|
||
<span className={styles.contactLabel}>Address</span>
|
||
<p>{business.contact.address}</p>
|
||
</div>
|
||
</div>
|
||
<div className={styles.contactRow}>
|
||
<span className={styles.contactIcon} aria-hidden>
|
||
☎
|
||
</span>
|
||
<div>
|
||
<span className={styles.contactLabel}>land line</span>
|
||
<p>{business.contact.landlines.join(" , ")}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className={styles.socials}>
|
||
{business.contact.socials.map((social) => (
|
||
<a
|
||
key={social.network}
|
||
href="#"
|
||
className={styles.socialChip}
|
||
>
|
||
<img src={social.icon} alt="" width={18} height={18} />
|
||
<span>
|
||
<em>{social.network}</em> {social.handle}
|
||
</span>
|
||
</a>
|
||
))}
|
||
</div>
|
||
<div className={styles.map}>
|
||
<Image
|
||
src={business.contact.map}
|
||
alt="Business location map"
|
||
fill
|
||
className={styles.mapImg}
|
||
sizes="100vw"
|
||
/>
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
|
||
{tab === "blog" ? (
|
||
<>
|
||
<CategoryPills
|
||
items={business.blogCategories}
|
||
active={blogCategory}
|
||
onChange={setBlogCategory}
|
||
/>
|
||
<FiltersBar
|
||
fields={[
|
||
{ label: "Article Name", value: "gold Scarlet necklace" },
|
||
{ label: "Date", value: "1402.05.12" },
|
||
]}
|
||
tags={[
|
||
{ label: "Name", value: "gold Scarlet necklace" },
|
||
{ label: "date", value: "1402.05.12" },
|
||
]}
|
||
/>
|
||
<div className={styles.blogGrid}>
|
||
{business.posts.map((post) => (
|
||
<Link
|
||
key={post.id}
|
||
href={`/businesses/${business.slug}/blog/${post.slug}`}
|
||
className={styles.blogCard}
|
||
>
|
||
<div className={styles.blogMedia}>
|
||
<Image
|
||
src={post.image}
|
||
alt=""
|
||
fill
|
||
className={styles.blogImg}
|
||
sizes="(max-width: 720px) 50vw, 292px"
|
||
/>
|
||
</div>
|
||
<div className={styles.blogBody}>
|
||
<h3>{post.title}</h3>
|
||
<p>{post.excerpt}</p>
|
||
<div className={styles.blogMeta}>
|
||
<span>
|
||
<img
|
||
src="/images/icons/home-outline.svg"
|
||
alt=""
|
||
width={14}
|
||
height={14}
|
||
/>
|
||
{post.date}
|
||
</span>
|
||
<span>
|
||
<img
|
||
src="/images/icons/user.svg"
|
||
alt=""
|
||
width={14}
|
||
height={14}
|
||
/>
|
||
{post.author}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</>
|
||
) : null}
|
||
|
||
{tab === "gallery" ? (
|
||
<div className={styles.gallery}>
|
||
{business.gallery.map((item) => (
|
||
<figure
|
||
key={item.id}
|
||
className={[
|
||
styles.galleryItem,
|
||
item.tall ? styles.galleryTall : "",
|
||
item.medium ? styles.galleryMedium : "",
|
||
]
|
||
.filter(Boolean)
|
||
.join(" ")}
|
||
>
|
||
<Image
|
||
src={item.image}
|
||
alt=""
|
||
fill
|
||
className={styles.galleryImg}
|
||
sizes="(max-width: 900px) 50vw, 20vw"
|
||
/>
|
||
<span className={styles.galleryZoom} aria-hidden>
|
||
<img src="/images/icons/search.svg" alt="" width={22} height={22} />
|
||
</span>
|
||
</figure>
|
||
))}
|
||
</div>
|
||
) : null}
|
||
|
||
{tab === "portfolios" ? (
|
||
<>
|
||
<CategoryPills
|
||
items={business.portfolioCategories}
|
||
active={portfolioCategory}
|
||
onChange={setPortfolioCategory}
|
||
/>
|
||
<div className={styles.portfolioGrid}>
|
||
{business.portfolios.map((item) => {
|
||
const showOverlay =
|
||
hoveredPortfolio === item.id ||
|
||
(hoveredPortfolio === null &&
|
||
item.id === business.portfolios[2]?.id);
|
||
return (
|
||
<Link
|
||
key={item.id}
|
||
href={`/businesses/${business.slug}/portfolios/${item.slug}`}
|
||
className={styles.portfolioCard}
|
||
onMouseEnter={() => setHoveredPortfolio(item.id)}
|
||
onMouseLeave={() => setHoveredPortfolio(null)}
|
||
>
|
||
<div className={styles.portfolioMedia}>
|
||
<Image
|
||
src={item.image}
|
||
alt=""
|
||
fill
|
||
className={styles.portfolioImg}
|
||
sizes="(max-width: 900px) 100vw, 33vw"
|
||
/>
|
||
<div
|
||
className={[
|
||
styles.portfolioOverlay,
|
||
showOverlay ? styles.portfolioOverlayVisible : "",
|
||
]
|
||
.filter(Boolean)
|
||
.join(" ")}
|
||
>
|
||
<h3>{item.title}</h3>
|
||
<p>{item.description}</p>
|
||
</div>
|
||
</div>
|
||
</Link>
|
||
);
|
||
})}
|
||
</div>
|
||
</>
|
||
) : null}
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|