Update carousels, card hover, and IranBay branding.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-10-04 13:21:51 +03:30
co-authored by Cursor
commit 42aff5aee9
162 changed files with 16828 additions and 0 deletions
@@ -0,0 +1,196 @@
.page {
background: var(--color-white);
}
.shell {
max-width: 1920px;
margin: 0 auto;
padding: 0 var(--page-pad) 80px;
}
.layout {
margin-top: 36px;
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.55fr);
gap: 42px;
align-items: start;
}
.article {
min-width: 0;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 18px 28px;
margin-bottom: 12px;
font-size: 12px;
font-weight: 300;
color: var(--color-soft);
}
.meta span {
display: inline-flex;
align-items: center;
gap: 8px;
}
.meta img {
width: 14px;
height: 14px;
opacity: 0.7;
object-fit: contain;
}
.title {
font-size: clamp(28px, 3vw, 40px);
font-weight: 300;
line-height: 1.2;
margin-bottom: 12px;
}
.intro {
font-size: 17px;
font-weight: 300;
line-height: 18px;
color: #9d9d9d;
text-align: justify;
margin-bottom: 28px;
}
.hero {
position: relative;
width: 100%;
aspect-ratio: 1190 / 525;
border: 1px solid #eee;
border-radius: 5px;
overflow: hidden;
margin-bottom: 36px;
background: #f6f6f6;
}
.heroImg {
object-fit: cover;
padding: 1.5%;
border-radius: 5px;
}
.sectionTitle {
font-size: clamp(28px, 3vw, 40px);
font-weight: 300;
margin-bottom: 18px;
}
.actions {
margin-top: 36px;
padding-top: 28px;
border-top: 1px solid #ececec;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 18px;
}
.related {
display: grid;
grid-template-columns: 154px minmax(0, 1fr);
gap: 18px;
padding: 18px;
border: 1px solid #eee;
border-radius: 5px;
transition: border-color 0.25s ease, transform 0.25s ease;
}
.related:hover {
border-color: #ccc;
transform: translateY(-2px);
}
.relatedMedia {
position: relative;
width: 154px;
height: 154px;
border-radius: 5px;
overflow: hidden;
background: #f6f6f6;
}
.relatedImg {
object-fit: cover;
}
.relatedBody {
display: flex;
flex-direction: column;
min-width: 0;
padding-top: 4px;
}
.relatedBody h3 {
font-size: 18px;
font-weight: 300;
margin-bottom: 10px;
}
.relatedBody p {
font-size: 14px;
font-weight: 300;
line-height: 1.45;
color: #9d9d9d;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 1;
}
.relatedMeta {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 14px;
font-size: 12px;
font-weight: 300;
color: var(--color-soft);
}
.relatedMeta span {
display: inline-flex;
align-items: center;
gap: 6px;
}
.relatedMeta img {
width: 14px;
height: 14px;
opacity: 0.55;
}
@media (max-width: 1100px) {
.layout {
grid-template-columns: 1fr;
}
.sidebar {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 720px) {
.sidebar {
grid-template-columns: 1fr;
}
.related {
grid-template-columns: 110px minmax(0, 1fr);
}
.relatedMedia {
width: 110px;
height: 110px;
}
}
@@ -0,0 +1,133 @@
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import DetailBanner from "@/components/DetailBanner";
import ArticleBody from "@/components/ArticleBody";
import ShareLikeActions from "@/components/ShareLikeActions";
import {
getAllBusinessSlugs,
getBusinessBlogPost,
getBusinessBySlug,
} from "@/data/business";
import styles from "./blog.module.css";
export function generateStaticParams() {
return getAllBusinessSlugs().flatMap((slug) => {
const business = getBusinessBySlug(slug);
return (business?.posts ?? []).map((post) => ({
slug,
postSlug: post.slug,
}));
});
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string; postSlug: string }>;
}) {
const { slug, postSlug } = await params;
const data = getBusinessBlogPost(slug, postSlug);
if (!data) return { title: "Blog Details | IranBay" };
return {
title: `${data.post.title} | Blog Details | IranBay`,
description: data.post.intro,
};
}
export default async function BlogDetailPage({
params,
}: {
params: Promise<{ slug: string; postSlug: string }>;
}) {
const { slug, postSlug } = await params;
const data = getBusinessBlogPost(slug, postSlug);
if (!data) notFound();
const { business, post, related } = data;
return (
<div className={styles.page}>
<Header />
<main className={styles.shell}>
<DetailBanner title="Blog Details" breadcrumbs={business.breadcrumbs} />
<div className={styles.layout}>
<article className={styles.article}>
<div className={styles.meta}>
<span>
<img src="/images/icons/calendar.svg" alt="" />
{post.date}
</span>
<span>
<img src="/images/icons/user.svg" alt="" />
{post.author}
</span>
<span>
<img src="/images/icons/category.svg" alt="" />
{post.category}
</span>
</div>
<h2 className={styles.title}>{post.title}.</h2>
<p className={styles.intro}>{post.intro}</p>
<div className={styles.hero}>
<Image
src={post.hero}
alt=""
fill
className={styles.heroImg}
sizes="(max-width: 1100px) 100vw, 1232px"
priority
/>
</div>
<h3 className={styles.sectionTitle}>{post.title}.</h3>
<ArticleBody blocks={post.body} />
<div className={styles.actions}>
<ShareLikeActions />
</div>
</article>
<aside className={styles.sidebar} aria-label="Related posts">
{related.map((item) => (
<Link
key={item.slug}
href={`/businesses/${business.slug}/blog/${item.slug}`}
className={styles.related}
>
<div className={styles.relatedMedia}>
<Image
src={item.image}
alt=""
fill
className={styles.relatedImg}
sizes="154px"
/>
</div>
<div className={styles.relatedBody}>
<h3>{item.title}</h3>
<p>{item.excerpt}</p>
<div className={styles.relatedMeta}>
<span>
<img src="/images/icons/calendar.svg" alt="" />
{item.date}
</span>
<span>
<img src="/images/icons/user.svg" alt="" />
{item.author}
</span>
</div>
</div>
</Link>
))}
</aside>
</div>
</main>
<Footer />
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
.page {
background: var(--color-white);
}
.shell {
max-width: 1920px;
margin: 0 auto;
padding: 0 var(--page-pad);
}
+52
View File
@@ -0,0 +1,52 @@
import { notFound } from "next/navigation";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import BusinessDetailView from "@/components/BusinessDetail";
import {
getAllBusinessSlugs,
getBusinessBySlug,
} from "@/data/business";
import styles from "./page.module.css";
export function generateStaticParams() {
return getAllBusinessSlugs().map((slug) => ({ slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const business = getBusinessBySlug(slug);
if (!business) {
return { title: "Business | IranBay" };
}
return {
title: `${business.name} | IranBay`,
description: business.description.slice(0, 140),
};
}
export default async function BusinessDetailPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const business = getBusinessBySlug(slug);
if (!business) {
notFound();
}
return (
<div className={styles.page}>
<Header />
<main className={styles.shell}>
<BusinessDetailView business={business} />
</main>
<Footer />
</div>
);
}
@@ -0,0 +1,92 @@
import { notFound } from "next/navigation";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import DetailBanner from "@/components/DetailBanner";
import ArticleBody from "@/components/ArticleBody";
import ShareLikeActions from "@/components/ShareLikeActions";
import PortfolioGallery from "@/components/PortfolioGallery";
import {
getAllBusinessSlugs,
getBusinessBySlug,
getBusinessPortfolio,
} from "@/data/business";
import styles from "./portfolio.module.css";
export function generateStaticParams() {
return getAllBusinessSlugs().flatMap((slug) => {
const business = getBusinessBySlug(slug);
return (business?.portfolios ?? []).map((item) => ({
slug,
portfolioSlug: item.slug,
}));
});
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string; portfolioSlug: string }>;
}) {
const { slug, portfolioSlug } = await params;
const data = getBusinessPortfolio(slug, portfolioSlug);
if (!data) return { title: "Portfolios details | IranBay" };
return {
title: `${data.portfolio.title} | Portfolios details | IranBay`,
description: data.portfolio.description,
};
}
export default async function PortfolioDetailPage({
params,
}: {
params: Promise<{ slug: string; portfolioSlug: string }>;
}) {
const { slug, portfolioSlug } = await params;
const data = getBusinessPortfolio(slug, portfolioSlug);
if (!data) notFound();
const { business, portfolio } = data;
return (
<div className={styles.page}>
<Header />
<main className={styles.shell}>
<DetailBanner
title="Portfolios details"
breadcrumbs={business.breadcrumbs}
/>
<div className={styles.layout}>
<PortfolioGallery images={portfolio.images} />
<div className={styles.copy}>
<div className={styles.meta}>
<span>
<img src="/images/icons/calendar.svg" alt="" />
{portfolio.date}
</span>
<span>
<img src="/images/icons/user.svg" alt="" />
{portfolio.author}
</span>
<span>
<img src="/images/icons/category.svg" alt="" />
{portfolio.category}
</span>
</div>
<h2 className={styles.title}>{portfolio.title}.</h2>
<p className={styles.intro}>{portfolio.description}</p>
<hr className={styles.rule} />
<div className={styles.bodyWrap}>
<ArticleBody blocks={portfolio.body} />
</div>
<div className={styles.actions}>
<ShareLikeActions />
</div>
</div>
</div>
</main>
<Footer />
</div>
);
}
@@ -0,0 +1,82 @@
.page {
background: var(--color-white);
}
.shell {
max-width: 1920px;
margin: 0 auto;
padding: 0 var(--page-pad) 80px;
}
.layout {
margin-top: 36px;
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
gap: 42px;
align-items: start;
}
.copy {
min-width: 0;
padding-top: 4px;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 18px 28px;
margin-bottom: 12px;
font-size: 12px;
font-weight: 300;
color: var(--color-soft);
}
.meta span {
display: inline-flex;
align-items: center;
gap: 8px;
}
.meta img {
width: 14px;
height: 14px;
opacity: 0.7;
object-fit: contain;
}
.title {
font-size: clamp(28px, 3vw, 40px);
font-weight: 300;
line-height: 1.2;
margin-bottom: 10px;
}
.intro {
font-size: 17px;
font-weight: 300;
line-height: 18px;
color: #9d9d9d;
margin-bottom: 18px;
}
.rule {
border: 0;
border-top: 1px solid #ececec;
margin: 0 0 20px;
}
.bodyWrap {
min-height: 280px;
}
.actions {
margin-top: 28px;
padding-top: 24px;
border-top: 1px solid #ececec;
}
@media (max-width: 980px) {
.layout {
grid-template-columns: 1fr;
}
}
+409
View File
@@ -0,0 +1,409 @@
.page {
background: var(--color-white);
}
.shell {
max-width: 1920px;
margin: 0 auto;
padding: 0 var(--page-pad);
}
.bannerWrap {
padding-top: 28px;
}
.banner {
position: relative;
isolation: isolate;
overflow: hidden;
background: #f6f6f6;
border-radius: 5px;
min-height: 136px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 24px 20px;
}
.banner::before {
content: "";
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
opacity: 0.7;
background-image: var(--page-title-pattern);
}
.bannerTitle,
.crumbs {
position: relative;
z-index: 1;
}
.bannerTitle {
font-size: clamp(24px, 2.2vw, 35px);
font-weight: 400;
letter-spacing: 14px;
text-transform: uppercase;
}
.crumbs {
margin-top: 14px;
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
justify-content: center;
font-size: 12px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--color-soft);
}
.crumbs > span {
display: inline-flex;
align-items: center;
gap: 10px;
}
.crumbChevron {
width: 10px;
height: 10px;
opacity: 0.55;
}
.crumbActive {
color: var(--color-black);
}
.container {
width: 100%;
}
.filtersBar {
margin-top: 28px;
background: #f9f9f9;
border-radius: 5px;
padding: 18px 24px;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 24px;
align-items: center;
}
.filtersLabel {
display: flex;
align-items: center;
gap: 12px;
min-width: 150px;
}
.filtersLabel strong {
display: block;
font-size: 18px;
font-weight: 500;
}
.filtersLabel span {
font-size: 12px;
color: var(--color-soft);
}
.filters {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)) auto;
gap: 12px;
}
.filters input {
width: 100%;
border: 1px solid #e4e4e4;
border-radius: 4px;
padding: 12px 14px;
background: var(--color-white);
font: inherit;
font-size: 13px;
}
.filterBtn {
height: 44px;
padding: 0 22px;
font-size: 14px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--color-soft);
}
.filterBtn img {
width: 10px;
height: 14px;
transition: filter 0.3s ease, opacity 0.3s ease;
}
:global(.filter-chip).filterBtn:hover,
:global(.filter-chip).filterBtn:focus-visible {
color: var(--color-white);
}
:global(.filter-chip).filterBtn:hover img,
:global(.filter-chip).filterBtn:focus-visible img {
filter: invert(1);
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 18px 0 28px;
}
.tag {
gap: 8px;
padding: 8px 12px;
font-size: 12px;
letter-spacing: 1px;
}
.tag button {
position: relative;
z-index: 2;
font-size: 14px;
line-height: 1;
color: var(--color-soft);
transition: color 0.3s ease;
}
:global(.filter-chip).tag:hover,
:global(.filter-chip).tag:focus-within {
color: var(--color-white);
}
:global(.filter-chip).tag:hover button,
:global(.filter-chip).tag:focus-within button {
color: var(--color-white);
}
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 20px;
}
.pager {
margin: 36px auto 56px;
display: flex;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.pager a,
.pager span {
min-width: 34px;
height: 34px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-size: 13px;
color: var(--color-soft);
}
.pagerActive {
background: var(--color-card);
color: var(--color-black) !important;
}
.featureSection {
max-width: var(--container-width);
margin: 0 auto;
width: 100%;
}
.feature {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 32px;
align-items: center;
padding: 48px 0 64px;
}
.featureTitle {
font-size: 36px;
font-weight: 400;
letter-spacing: 0;
text-transform: uppercase;
line-height: 1.1;
}
.featureLead {
margin-top: 6px;
font-size: 22px;
font-weight: 400;
color: var(--color-soft);
}
.featureBody {
margin-top: 16px;
font-size: 15px;
font-weight: 400;
line-height: 1.4;
color: var(--color-body);
max-width: none;
}
.featureList {
list-style: none;
margin-top: 18px;
display: flex;
flex-direction: column;
gap: 10px;
}
.featureList li {
display: flex;
gap: 10px;
align-items: flex-start;
font-size: 15px;
color: var(--color-body);
}
.featureList img {
width: 9px;
height: 11px;
margin-top: 6px;
flex-shrink: 0;
}
.featureCta {
margin-top: 22px;
height: 34px;
padding: 0 16px;
font-size: 13px;
letter-spacing: 2.4px;
}
.featureVisual {
position: relative;
background: var(--color-card);
border-radius: 10px;
min-height: 360px;
aspect-ratio: 1 / 1.05;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 28px;
}
.featureVisual img {
width: auto;
height: auto;
max-width: 72%;
max-height: 86%;
object-fit: contain;
}
.exportSection {
margin: 0 0 72px;
}
.exportCta {
background: var(--color-card);
border-radius: 5px;
height: min(201px, calc((min(100vw, 1920px) - 2 * var(--page-pad)) * 201 / 1800));
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 0 40px 0 22%;
position: relative;
overflow: hidden;
}
.plane {
position: absolute;
left: 1.5%;
top: 50%;
width: min(240px, 16%);
height: auto;
max-height: 88%;
object-fit: contain;
transform: translateY(-52%) rotate(38deg);
transform-origin: center;
pointer-events: none;
}
.exportTitle {
font-size: clamp(28px, 4vw, 48px);
font-weight: 300;
letter-spacing: 8px;
text-transform: uppercase;
line-height: 1.05;
}
.beginBtn {
height: 40px;
padding: 0 18px;
gap: 10px;
font-size: 13px;
letter-spacing: 3px;
}
.beginBtn img {
width: 9px;
height: 16px;
}
@media (max-width: 1200px) {
.grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.filtersBar {
grid-template-columns: 1fr;
}
.filters {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 980px) {
.feature {
grid-template-columns: 1fr;
}
.exportCta {
flex-direction: column;
justify-content: center;
text-align: center;
height: auto;
min-height: 160px;
padding: 28px 20px 24px;
}
.plane {
position: relative;
left: auto;
top: auto;
width: min(180px, 46%);
max-height: 120px;
transform: rotate(38deg);
margin-bottom: 8px;
}
}
@media (max-width: 720px) {
.grid,
.filters {
grid-template-columns: 1fr;
}
.bannerTitle {
letter-spacing: 6px;
}
}
+162
View File
@@ -0,0 +1,162 @@
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import ProducerCard from "@/components/ProducerCard";
import { aboutBullets, featuredProducers } from "@/data/home";
import {
activeFilterTags,
breadcrumbs,
businessFilters,
} from "@/data/pages";
import styles from "./businesses.module.css";
export const metadata = {
title: "Businesses | IranBay",
};
export default function BusinessesPage() {
const cards = [
...featuredProducers,
...featuredProducers,
...featuredProducers,
...featuredProducers,
].slice(0, 16);
return (
<div className={styles.page}>
<Header />
<main className={styles.shell}>
<section className={styles.bannerWrap}>
<div className={styles.banner}>
<h1 className={styles.bannerTitle}>Let’s explore</h1>
<nav className={styles.crumbs} aria-label="Breadcrumb">
{breadcrumbs.map((crumb, index) => (
<span key={crumb}>
{index > 0 ? (
<img
className={styles.crumbChevron}
src="/images/icons/chevron.svg"
alt=""
/>
) : null}
<span
className={
index === breadcrumbs.length - 1 ? styles.crumbActive : undefined
}
>
{crumb}
</span>
</span>
))}
</nav>
</div>
</section>
<section className={styles.container}>
<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}>
{businessFilters.map((filter) => (
<input
key={filter.label}
aria-label={filter.label}
defaultValue={filter.value}
/>
))}
<button className={`filter-chip ${styles.filterBtn}`} type="button">
Filter
<img src="/images/icons/chevron.svg" alt="" />
</button>
</div>
</div>
<div className={styles.tags}>
{activeFilterTags.map((tag) => (
<span className={`filter-chip ${styles.tag}`} key={tag}>
{tag}
<button type="button" aria-label={`Remove ${tag}`}>
×
</button>
</span>
))}
</div>
<div className={styles.grid}>
{cards.map((producer, index) => (
<ProducerCard
key={`${producer.name}-${index}`}
producer={producer}
/>
))}
</div>
<nav className={styles.pager} aria-label="Pagination">
<a href="/businesses">First</a>
<a href="/businesses">1</a>
<span className={styles.pagerActive}>2</span>
<a href="/businesses">3</a>
<span>…</span>
<a href="/businesses">15</a>
<a href="/businesses">Last</a>
</nav>
</section>
<section className={styles.featureSection}>
<div className={styles.feature}>
<div>
<h2 className={styles.featureTitle}>Processed foods,</h2>
<p className={styles.featureLead}>
We are a gate to Iran&apos;s market place
</p>
<p className={styles.featureBody}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat.
</p>
<ul className={styles.featureList}>
{aboutBullets.map((item, index) => (
<li key={`${item}-${index}`}>
<img src="/images/icons/bullet.svg" alt="" />
<span>{item}</span>
</li>
))}
</ul>
<a className={`outline-btn ${styles.featureCta}`} href="/about">
know more about us
</a>
</div>
<div className={styles.featureVisual}>
<img src="/images/hero/ice-cream.png" alt="Processed foods" />
</div>
</div>
</section>
<section className={styles.exportSection}>
<div className={`tile-hover ${styles.exportCta}`}>
<img
className={styles.plane}
src="/images/banners/plane.png"
alt=""
/>
<h2 className={styles.exportTitle}>
let’s
<br />
export
</h2>
<a className={`outline-btn ${styles.beginBtn}`} href="/contact">
begin
<img src="/images/icons/chevron.svg" alt="" width={9} height={16} />
</a>
</div>
</section>
</main>
<Footer />
</div>
);
}