Update carousels, card hover, and IranBay branding.
Co-authored-by: Cursor <cursoragent@cursor.com>
@@ -0,0 +1,16 @@
|
||||
NEXT_PUBLIC_DASHBOARD_URL=http://localhost:3002
|
||||
NEXT_PUBLIC_ADMIN_DASHBOARD_URL=http://localhost:3002
|
||||
NEXT_PUBLIC_BUSINESS_DASHBOARD_URL=http://business.localhost:3002
|
||||
NEXT_PUBLIC_WEBSITE_URL=http://localhost:3001
|
||||
NEXT_PUBLIC_CUSTOMER_DASHBOARD_URL=http://customer.localhost:3002
|
||||
NEXT_PUBLIC_ROOT_DOMAIN=localhost
|
||||
# Meshkee — always remote (do not point at a local Meshkee)
|
||||
NEXT_PUBLIC_MESHKEE_API_URL=https://api.meshkee.com/api/v1
|
||||
|
||||
# Meshkee (used by /api/meshkee/categories)
|
||||
MESHKEE_API_URL=https://api.meshkee.com/api/v1
|
||||
# Prefer cell + password (access tokens expire quickly). Optional static token:
|
||||
# MESHKEE_API_TOKEN=
|
||||
# MESHKEE_SUPERADMIN_CELL=
|
||||
# MESHKEE_SUPERADMIN_PASSWORD=
|
||||
NEXT_PUBLIC_MESHKEE_TENANT_DOMAIN=iranbay.com
|
||||
@@ -0,0 +1,42 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,9 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
|
||||
# This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||
|
||||
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
@@ -0,0 +1,13 @@
|
||||
# IranBay Website
|
||||
|
||||
Public Next.js site. From this folder:
|
||||
|
||||
```bash
|
||||
cp .env.example .env.local
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
Account login is on the dashboard app (`NEXT_PUBLIC_DASHBOARD_URL`, default `http://localhost:3002`). Business registration stays at `/register`.
|
||||
@@ -0,0 +1,369 @@
|
||||
.page {
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: var(--container-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--page-pad);
|
||||
}
|
||||
|
||||
.wide {
|
||||
max-width: 1512px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--page-pad);
|
||||
}
|
||||
|
||||
.intro {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 0.85fr) minmax(0, 1.15fr);
|
||||
gap: 48px;
|
||||
align-items: center;
|
||||
padding: 56px 0 72px;
|
||||
}
|
||||
|
||||
.visual {
|
||||
position: relative;
|
||||
width: min(100%, 380px);
|
||||
aspect-ratio: 466 / 634;
|
||||
margin-inline: auto 0;
|
||||
}
|
||||
|
||||
.visualBack {
|
||||
position: absolute;
|
||||
inset: 0 18% 12% 0;
|
||||
background: var(--color-card);
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
.visualCard {
|
||||
position: absolute;
|
||||
left: 14%;
|
||||
top: 12%;
|
||||
width: 86%;
|
||||
height: 88%;
|
||||
border-radius: 32px;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.kicker {
|
||||
font-size: clamp(22px, 2.2vw, 35px);
|
||||
font-weight: 300;
|
||||
color: #434343;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: clamp(34px, 3.4vw, 55px);
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.title span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.copy {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
color: #bababa;
|
||||
margin-bottom: 18px;
|
||||
max-width: 650px;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.cta {
|
||||
margin-top: 18px;
|
||||
height: 34px;
|
||||
padding: 0 16px;
|
||||
min-width: 207px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 2.4px;
|
||||
}
|
||||
|
||||
.clients {
|
||||
padding: 24px 0 56px;
|
||||
}
|
||||
|
||||
.clientBar {
|
||||
margin-top: 36px;
|
||||
background: #f8f8f8;
|
||||
border-radius: 5px;
|
||||
min-height: 160px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 24px 40px;
|
||||
}
|
||||
|
||||
.clientLogo {
|
||||
width: 100%;
|
||||
max-height: 96px;
|
||||
object-fit: contain;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.story {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 0.85fr) minmax(0, 1.15fr);
|
||||
gap: 40px;
|
||||
padding: 40px 0 72px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.story .cta {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.comments {
|
||||
padding: 24px 0 72px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.commentHead {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.commentHead .sectionHead {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.arrows {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.arrowBtn {
|
||||
width: 38px;
|
||||
height: 36px;
|
||||
background: var(--color-card);
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.arrowBtn img {
|
||||
width: 8px;
|
||||
height: 14px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.arrowPrev {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.commentCount {
|
||||
font-size: 13px;
|
||||
letter-spacing: 1px;
|
||||
color: var(--color-soft);
|
||||
min-width: 48px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.commentTrack {
|
||||
animation: commentIn 0.35s ease;
|
||||
}
|
||||
|
||||
@keyframes commentIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.quote {
|
||||
max-width: 900px;
|
||||
margin: 28px auto 18px;
|
||||
font-size: 18px;
|
||||
line-height: 1.45;
|
||||
color: #bababa;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 18px;
|
||||
color: #5f5f5f;
|
||||
}
|
||||
|
||||
.dots {
|
||||
margin-top: 28px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dot,
|
||||
.dotActive {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #d8d8d8;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dotActive {
|
||||
background: var(--color-black);
|
||||
}
|
||||
|
||||
.services {
|
||||
padding: 24px 0 96px;
|
||||
}
|
||||
|
||||
.serviceGrid {
|
||||
margin-top: 48px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 28px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.serviceCard {
|
||||
position: relative;
|
||||
min-height: 190px;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: end;
|
||||
gap: 8px;
|
||||
padding: 24px 16px 18px 0;
|
||||
}
|
||||
|
||||
.serviceCard::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 52px;
|
||||
right: 0;
|
||||
bottom: 18px;
|
||||
height: 135px;
|
||||
background: #f8f8f8;
|
||||
border-radius: 25px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.serviceCard img {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 160px;
|
||||
max-width: 150px;
|
||||
object-fit: contain;
|
||||
margin-left: 0;
|
||||
filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
.serviceCopy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
align-self: center;
|
||||
padding: 8px 12px 8px 4px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.serviceCopy h3 {
|
||||
font-size: clamp(20px, 1.8vw, 30px);
|
||||
font-weight: 400;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.serviceCopy p {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1.2px;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.support .serviceCopy h3 {
|
||||
color: #575f50;
|
||||
}
|
||||
|
||||
.register .serviceCopy h3 {
|
||||
color: #b68968;
|
||||
}
|
||||
|
||||
.responsive .serviceCopy h3 {
|
||||
color: #474039;
|
||||
}
|
||||
|
||||
.sectionHead {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: clamp(24px, 2.2vw, 35px);
|
||||
font-weight: 400;
|
||||
letter-spacing: 14px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sectionSub {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 4.02px;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-soft);
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.intro,
|
||||
.story {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.visual {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.commentHead {
|
||||
grid-template-columns: 1fr;
|
||||
justify-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.commentCount {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.serviceGrid {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 420px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.clientBar {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.clientBar {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
letter-spacing: 6px;
|
||||
}
|
||||
|
||||
.serviceCopy {
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import CommentCarousel from "@/components/CommentCarousel";
|
||||
import { aboutCopy, clients, services } from "@/data/pages";
|
||||
import styles from "./about.module.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "About Us | IranBay",
|
||||
};
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<Header />
|
||||
<main>
|
||||
<section className={styles.container}>
|
||||
<div className={styles.intro}>
|
||||
<div className={styles.visual}>
|
||||
<div className={styles.visualBack} />
|
||||
<img
|
||||
className={styles.visualCard}
|
||||
src="/images/about/hero-card.png"
|
||||
alt="IranBay"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className={styles.kicker}>{aboutCopy.kicker}</p>
|
||||
<h1 className={styles.title}>
|
||||
{aboutCopy.title.map((line) => (
|
||||
<span key={line}>{line}</span>
|
||||
))}
|
||||
</h1>
|
||||
{aboutCopy.paragraphs.map((p) => (
|
||||
<p className={styles.copy} key={p.slice(0, 32)}>
|
||||
{p}
|
||||
</p>
|
||||
))}
|
||||
<a className={`outline-btn ${styles.cta}`} href="/contact">
|
||||
Contact us
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.wide}>
|
||||
<div className={styles.clients}>
|
||||
<div className={styles.sectionHead}>
|
||||
<h2 className={styles.sectionTitle}>All clinets</h2>
|
||||
<p className={styles.sectionSub}>
|
||||
some of our most crowded categories
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.clientBar}>
|
||||
{clients.map((client) => (
|
||||
<img
|
||||
key={client.name}
|
||||
src={client.image}
|
||||
alt={client.name}
|
||||
className={styles.clientLogo}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.container}>
|
||||
<div className={styles.story}>
|
||||
<div>
|
||||
<p className={styles.kicker}>{aboutCopy.kicker}</p>
|
||||
<h2 className={styles.title}>
|
||||
{aboutCopy.title.map((line) => (
|
||||
<span key={`story-${line}`}>{line}</span>
|
||||
))}
|
||||
</h2>
|
||||
</div>
|
||||
<div>
|
||||
<p className={styles.copy}>{aboutCopy.paragraphs[0]}</p>
|
||||
<a className={`outline-btn ${styles.cta}`} href="/contact">
|
||||
Contact us
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.container}>
|
||||
<CommentCarousel />
|
||||
</section>
|
||||
|
||||
<section className={styles.wide}>
|
||||
<div className={styles.services}>
|
||||
<div className={styles.sectionHead}>
|
||||
<h2 className={styles.sectionTitle}>Our services</h2>
|
||||
<p className={styles.sectionSub}>
|
||||
some of our most crowded categories
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.serviceGrid}>
|
||||
{services.map((item) => (
|
||||
<article
|
||||
className={`${styles.serviceCard} ${styles[item.tone]}`}
|
||||
key={item.title}
|
||||
>
|
||||
<img src={item.image} alt="" />
|
||||
<div className={styles.serviceCopy}>
|
||||
<h3>{item.title}</h3>
|
||||
<p>{item.subtitle}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const MESHKEE_API_URL =
|
||||
process.env.MESHKEE_API_URL?.replace(/\/$/, "") ||
|
||||
process.env.NEXT_PUBLIC_MESHKEE_API_URL?.replace(/\/$/, "") ||
|
||||
"https://api.meshkee.com/api/v1";
|
||||
|
||||
type CachedToken = { token: string; expiresAt: number };
|
||||
|
||||
let cachedToken: CachedToken | null = null;
|
||||
|
||||
function tokenExpiry(token: string) {
|
||||
try {
|
||||
const payload = JSON.parse(
|
||||
Buffer.from(token.split(".")[1] ?? "", "base64url").toString(),
|
||||
) as { exp?: number };
|
||||
if (typeof payload.exp === "number") return payload.exp * 1000;
|
||||
} catch {
|
||||
/* ignore malformed tokens */
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function usableStaticToken() {
|
||||
const token = process.env.MESHKEE_API_TOKEN?.trim();
|
||||
if (!token) return null;
|
||||
const expiresAt = tokenExpiry(token);
|
||||
if (expiresAt && expiresAt < Date.now() + 15_000) return null;
|
||||
return token;
|
||||
}
|
||||
|
||||
async function serviceToken(): Promise<string | null> {
|
||||
const now = Date.now();
|
||||
if (cachedToken && cachedToken.expiresAt > now + 15_000) {
|
||||
return cachedToken.token;
|
||||
}
|
||||
|
||||
const staticToken = usableStaticToken();
|
||||
if (staticToken) return staticToken;
|
||||
|
||||
const cellNumber = process.env.MESHKEE_SUPERADMIN_CELL?.trim();
|
||||
const password = process.env.MESHKEE_SUPERADMIN_PASSWORD?.trim();
|
||||
if (!cellNumber || !password) return null;
|
||||
|
||||
const response = await fetch(`${MESHKEE_API_URL}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ cellNumber, password }),
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
|
||||
const payload = (await response.json().catch(() => null)) as {
|
||||
accessToken?: string;
|
||||
} | null;
|
||||
const token = payload?.accessToken?.trim();
|
||||
if (!token) return null;
|
||||
|
||||
cachedToken = {
|
||||
token,
|
||||
expiresAt: tokenExpiry(token) || now + 10 * 60 * 1000,
|
||||
};
|
||||
return token;
|
||||
}
|
||||
|
||||
async function authHeader(request: NextRequest): Promise<string | null> {
|
||||
const incoming = request.headers.get("authorization")?.trim();
|
||||
if (incoming) return incoming;
|
||||
|
||||
const token = await serviceToken();
|
||||
return token ? `Bearer ${token}` : null;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await authHeader(request);
|
||||
if (!auth) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
message:
|
||||
"Meshkee categories need auth. Set MESHKEE_API_TOKEN or sign in on the dashboard first.",
|
||||
},
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
const q = request.nextUrl.searchParams.get("q")?.trim() ?? "";
|
||||
const params = new URLSearchParams();
|
||||
if (q) params.set("q", q);
|
||||
|
||||
const response = await fetch(
|
||||
`${MESHKEE_API_URL}/business-categories?${params}`,
|
||||
{
|
||||
headers: { Authorization: auth },
|
||||
cache: "no-store",
|
||||
},
|
||||
);
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
payload ?? { message: `Meshkee error (${response.status})` },
|
||||
{ status: response.status },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(payload);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Install Meshkee session from the URL hash before the register page reads tokens.
|
||||
* Used after customer-dashboard login with ?next=https://…/register
|
||||
*/
|
||||
const HANDOFF_BOOT = `
|
||||
(function () {
|
||||
try {
|
||||
var raw = location.hash ? location.hash.replace(/^#/, "") : "";
|
||||
if (!raw) return;
|
||||
var p = new URLSearchParams(raw);
|
||||
var access = p.get("access");
|
||||
var refresh = p.get("refresh");
|
||||
var next = p.get("next") || "/register";
|
||||
if (!access || !refresh) return;
|
||||
|
||||
localStorage.setItem("marketplace_access_token", access);
|
||||
localStorage.setItem("marketplace_refresh_token", refresh);
|
||||
|
||||
var root = "localhost";
|
||||
try {
|
||||
document.cookie =
|
||||
"marketplace_access_token=" +
|
||||
encodeURIComponent(access) +
|
||||
"; Path=/; Max-Age=1209600; SameSite=Lax; Domain=" +
|
||||
root;
|
||||
document.cookie =
|
||||
"marketplace_refresh_token=" +
|
||||
encodeURIComponent(refresh) +
|
||||
"; Path=/; Max-Age=1209600; SameSite=Lax; Domain=" +
|
||||
root;
|
||||
} catch (e) {}
|
||||
|
||||
var target = next.charAt(0) === "/" ? next : "/" + next;
|
||||
location.replace(target);
|
||||
} catch (e) {}
|
||||
})();
|
||||
`;
|
||||
|
||||
export default function AuthHandoffLayout({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<script dangerouslySetInnerHTML={{ __html: HANDOFF_BOOT }} />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
export default function AuthHandoffPage() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
background: "#f6f6f6",
|
||||
fontSize: 12,
|
||||
letterSpacing: "0.2em",
|
||||
textTransform: "uppercase",
|
||||
color: "#8e8e8e",
|
||||
padding: 24,
|
||||
textAlign: "center",
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
Signing you in…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.page {
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
.shell {
|
||||
max-width: 1920px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--page-pad);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
.page {
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
.bannerWrap {
|
||||
max-width: 1920px;
|
||||
margin: 0 auto;
|
||||
padding: 28px var(--page-pad) 0;
|
||||
}
|
||||
|
||||
.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: 28px 20px;
|
||||
}
|
||||
|
||||
.banner::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
background-image: var(--page-title-pattern);
|
||||
}
|
||||
|
||||
.bannerTitle,
|
||||
.bannerSub {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bannerTitle {
|
||||
font-size: clamp(24px, 2.2vw, 35px);
|
||||
font-weight: 400;
|
||||
letter-spacing: 14px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.bannerSub {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 4.02px;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-soft);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1552px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--page-pad);
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(240px, 0.85fr);
|
||||
gap: 64px;
|
||||
padding: 56px 0 40px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.panel::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 56px;
|
||||
bottom: 40px;
|
||||
left: 58%;
|
||||
width: 2px;
|
||||
background: var(--color-card);
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: clamp(36px, 3.4vw, 55px);
|
||||
font-weight: 700;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.sendLabel {
|
||||
font-size: clamp(22px, 2vw, 35px);
|
||||
font-weight: 400;
|
||||
letter-spacing: 7px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sendHint {
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 2.4px;
|
||||
text-transform: uppercase;
|
||||
color: #4b4b4b;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 28px;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
max-width: 761px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.field {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.field span {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 16px;
|
||||
z-index: 1;
|
||||
background: var(--color-white);
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
color: #aaa;
|
||||
transition: color 0.25s ease;
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field select,
|
||||
.field textarea {
|
||||
width: 100%;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
background: var(--color-white);
|
||||
padding: 14px 18px;
|
||||
font: inherit;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
color: var(--color-black);
|
||||
outline: 0;
|
||||
transition: border-color 0.25s ease;
|
||||
}
|
||||
|
||||
.field:focus-within span {
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.field input:focus,
|
||||
.field select:focus,
|
||||
.field textarea:focus {
|
||||
border-color: var(--color-black);
|
||||
}
|
||||
|
||||
.field select {
|
||||
appearance: none;
|
||||
background-image: url("/images/icons/chevron.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 18px center;
|
||||
background-size: 8px 14px;
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
.plain span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.field textarea {
|
||||
min-height: 231px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.formActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.formActions .outline-btn,
|
||||
.formActions button {
|
||||
height: 29px;
|
||||
min-width: 207px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 2.4px;
|
||||
}
|
||||
|
||||
.aside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 36px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.asideBlock h3 {
|
||||
font-size: clamp(24px, 2vw, 35px);
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.asideBlock p,
|
||||
.asideBlock a {
|
||||
font-size: clamp(16px, 1.4vw, 25px);
|
||||
font-weight: 300;
|
||||
color: #4b4b4b;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.asideBlock p {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.phones {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.socials {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.socials img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.mapWrap {
|
||||
padding: 12px 0 56px;
|
||||
}
|
||||
|
||||
.mapWrap img {
|
||||
width: 100%;
|
||||
height: clamp(220px, 32vw, 360px);
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.faq {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 0.7fr) minmax(0, 1.3fr);
|
||||
gap: 48px;
|
||||
padding: 12px 0 96px;
|
||||
}
|
||||
|
||||
.faqKicker {
|
||||
font-size: 20px;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.faqTitle {
|
||||
font-size: clamp(34px, 3.4vw, 55px);
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.faqList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.faqItem {
|
||||
padding: 12px 0 18px;
|
||||
}
|
||||
|
||||
.faqTrigger {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
text-align: left;
|
||||
font-size: clamp(20px, 1.8vw, 30px);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.faqOpen {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.faqClosed {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.faqAnswer {
|
||||
margin-top: 14px;
|
||||
font-size: 18px;
|
||||
line-height: 1.35;
|
||||
color: #939393;
|
||||
max-width: 703px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.panel::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.panel,
|
||||
.faq {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.bannerTitle {
|
||||
letter-spacing: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import ContactFaq from "@/components/ContactFaq";
|
||||
import { contactInfo } from "@/data/pages";
|
||||
import styles from "./contact.module.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "Contact Us | IranBay",
|
||||
};
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<Header />
|
||||
<main>
|
||||
<section className={styles.bannerWrap}>
|
||||
<div className={styles.banner}>
|
||||
<h1 className={styles.bannerTitle}>Contact us page</h1>
|
||||
<p className={styles.bannerSub}>
|
||||
some of our most crowded categories
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.container}>
|
||||
<div className={styles.panel}>
|
||||
<div>
|
||||
<h2 className={styles.heading}>Get In Touch</h2>
|
||||
<p className={styles.sendLabel}>Send message</p>
|
||||
<p className={styles.sendHint}>
|
||||
In publishing and graphic design, Lorem ipsum is a placeholder
|
||||
text commonly used to.
|
||||
</p>
|
||||
<form className={styles.form}>
|
||||
<div className={styles.row}>
|
||||
<label className={styles.field}>
|
||||
<span>Name</span>
|
||||
<input name="name" defaultValue="Arad Charta" />
|
||||
</label>
|
||||
<label className={styles.field}>
|
||||
<span>Family</span>
|
||||
<input name="family" defaultValue="Arad Charta" />
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<label className={styles.field}>
|
||||
<span>Interested in</span>
|
||||
<select name="interest" defaultValue="Developer">
|
||||
<option>Developer</option>
|
||||
<option>Producer</option>
|
||||
<option>Store</option>
|
||||
<option>Service</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className={`${styles.field} ${styles.plain}`}>
|
||||
<input
|
||||
name="phone"
|
||||
placeholder="Phone Number"
|
||||
aria-label="Phone Number"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label className={styles.field}>
|
||||
<span>Message</span>
|
||||
<textarea name="message" placeholder="Description" />
|
||||
</label>
|
||||
<div className={styles.formActions}>
|
||||
<button className="outline-btn" type="submit">
|
||||
Submit message
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<aside className={styles.aside}>
|
||||
<div className={styles.asideBlock}>
|
||||
<h3>Email Us :</h3>
|
||||
<a href={`mailto:${contactInfo.email}`}>{contactInfo.email}</a>
|
||||
</div>
|
||||
<div className={styles.asideBlock}>
|
||||
<h3>Call Us :</h3>
|
||||
<div className={styles.phones}>
|
||||
{contactInfo.phones.map((phone) => (
|
||||
<p key={phone}>{phone}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.asideBlock}>
|
||||
<h3>Follow :</h3>
|
||||
<div className={styles.socials}>
|
||||
<a href="https://instagram.com" aria-label="Instagram">
|
||||
<img src="/images/icons/instagram.png" alt="" />
|
||||
</a>
|
||||
<a href="https://t.me" aria-label="Telegram">
|
||||
<img src="/images/icons/telegram.png" alt="" />
|
||||
</a>
|
||||
<a href="https://facebook.com" aria-label="Facebook">
|
||||
<img src="/images/icons/facebook.png" alt="" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.asideBlock}>
|
||||
<h3>Visit Us :</h3>
|
||||
{contactInfo.addressLines.map((line) => (
|
||||
<p key={line}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.container}>
|
||||
<div className={styles.mapWrap}>
|
||||
<img src="/images/contact/map.png" alt="Office location map" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.container}>
|
||||
<div className={styles.faq}>
|
||||
<div>
|
||||
<p className={styles.faqKicker}>FAQ</p>
|
||||
<h2 className={styles.faqTitle}>
|
||||
Frequently
|
||||
<br />
|
||||
asked
|
||||
<br />
|
||||
questions.
|
||||
</h2>
|
||||
</div>
|
||||
<ContactFaq />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,274 @@
|
||||
:root {
|
||||
--font-montserrat: "Montserrat", sans-serif;
|
||||
--color-black: #000;
|
||||
--color-white: #fff;
|
||||
--color-bg: #ffffff;
|
||||
--color-card: #eeeeee;
|
||||
--color-muted: #4a4a4a;
|
||||
--color-soft: #8e8e8e;
|
||||
--color-body: #aaaaaa;
|
||||
--color-dark: #1e1e1e;
|
||||
--color-explore: #f9f9f9;
|
||||
--color-footer: #070707;
|
||||
--color-footer-text: #bababa;
|
||||
--color-footer-line: #232323;
|
||||
--page-pad: 60px;
|
||||
--max-width: 1920px;
|
||||
--container-width: 1080px;
|
||||
--page-title-pattern:
|
||||
repeating-linear-gradient(
|
||||
135deg,
|
||||
rgba(0, 0, 0, 0.045) 0 1px,
|
||||
transparent 1px 16px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
45deg,
|
||||
rgba(0, 0, 0, 0.03) 0 1px,
|
||||
transparent 1px 16px
|
||||
);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
color: var(--color-black);
|
||||
background: var(--color-bg);
|
||||
font-family: var(--font-montserrat), "Montserrat", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
button,
|
||||
a {
|
||||
font-family: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.outline-btn {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0.5px solid var(--color-black);
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
color: var(--color-black);
|
||||
font-weight: 300;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
white-space: nowrap;
|
||||
transition: color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.outline-btn:hover,
|
||||
.outline-btn:focus-visible {
|
||||
color: var(--color-white);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.outline-btn:hover :is(span, strong, em, p, a, label, button),
|
||||
.outline-btn:focus-visible :is(span, strong, em, p, a, label, button) {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 33px;
|
||||
padding: 6px 22px;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background: #f6f6f6;
|
||||
color: var(--color-black);
|
||||
font: inherit;
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease, background 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-chip:hover,
|
||||
.filter-chip:focus-visible {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.filter-chip:hover :is(span, strong, em, p, a, label, button),
|
||||
.filter-chip:focus-visible :is(span, strong, em, p, a, label, button) {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.filter-chip.is-active {
|
||||
background: var(--color-black);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.filter-chip.is-active:hover,
|
||||
.filter-chip.is-active:focus-visible {
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.filter-chip.is-active:hover :is(span, strong, em, p, a, label, button),
|
||||
.filter-chip.is-active:focus-visible :is(span, strong, em, p, a, label, button) {
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.tile-hover,
|
||||
.outline-btn,
|
||||
.filter-chip {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.tile-hover::before,
|
||||
.outline-btn::before,
|
||||
.filter-chip::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--color-black);
|
||||
clip-path: inset(100% 0 0 0);
|
||||
transition: clip-path 0.45s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
/* Tiles: wipe sits above bare text; children are raised below. */
|
||||
.tile-hover::before {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Buttons/chips often use direct text nodes (no child wrappers).
|
||||
Keep the wipe behind the label so white text stays visible.
|
||||
*/
|
||||
.outline-btn::before,
|
||||
.filter-chip::before {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.filter-chip.is-active::before {
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
.tile-hover:hover::before,
|
||||
.tile-hover:focus-within::before,
|
||||
.outline-btn:hover::before,
|
||||
.outline-btn:focus-visible::before,
|
||||
.filter-chip:hover::before,
|
||||
.filter-chip:focus-visible::before {
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
|
||||
.tile-hover > *:not(img),
|
||||
.outline-btn > *,
|
||||
.filter-chip > * {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tile-hover:hover,
|
||||
.tile-hover:focus-within,
|
||||
.tile-hover:hover :is(h1, h2, h3, p, span, strong, a),
|
||||
.tile-hover:focus-within :is(h1, h2, h3, p, span, strong, a) {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.tile-hover :is(h1, h2, h3, p, span, strong, a) {
|
||||
transition: color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.tile-hover:hover .outline-btn,
|
||||
.tile-hover:hover .outline-btn:hover,
|
||||
.tile-hover:focus-within .outline-btn {
|
||||
color: var(--color-white);
|
||||
border-color: var(--color-white);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tile-hover:hover .outline-btn::before,
|
||||
.tile-hover:focus-within .outline-btn::before {
|
||||
clip-path: inset(100% 0 0 0);
|
||||
}
|
||||
|
||||
.outline-btn:hover img,
|
||||
.outline-btn:focus-visible img,
|
||||
.filter-chip:hover img,
|
||||
.filter-chip:focus-visible img {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.tile-hover::before,
|
||||
.outline-btn::before,
|
||||
.filter-chip::before {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
:root {
|
||||
--page-pad: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
:root {
|
||||
--page-pad: 20px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,815 @@
|
||||
.page {
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
.section {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--page-pad);
|
||||
scroll-margin-top: 80px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1080px;
|
||||
}
|
||||
|
||||
.container + .container,
|
||||
.section + .container {
|
||||
margin-top: 96px;
|
||||
}
|
||||
|
||||
.carousel {
|
||||
margin-top: 96px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.95fr) minmax(220px, 300px) minmax(220px, 300px);
|
||||
gap: 7px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.heroCard {
|
||||
position: relative;
|
||||
background: var(--color-card);
|
||||
border-radius: 5px;
|
||||
min-height: 526px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding: 28px 24px 28px;
|
||||
}
|
||||
|
||||
.heroMain {
|
||||
padding: 0 72px 48px 102px;
|
||||
}
|
||||
|
||||
.heroCopy {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 430px;
|
||||
}
|
||||
|
||||
.heroTitle {
|
||||
font-size: 40px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 8px;
|
||||
text-transform: uppercase;
|
||||
line-height: 45px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.heroTitleSmall {
|
||||
font-size: 25px;
|
||||
letter-spacing: 5px;
|
||||
line-height: 30px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.heroBtn {
|
||||
height: 37px;
|
||||
padding: 0 20px;
|
||||
font-size: 16px;
|
||||
letter-spacing: 3.2px;
|
||||
}
|
||||
|
||||
.heroBtnSmall {
|
||||
height: 29px;
|
||||
padding: 0 16px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 2.4px;
|
||||
}
|
||||
|
||||
.heroImage {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.heroFurniture {
|
||||
width: min(513px, 52%);
|
||||
height: auto;
|
||||
right: 4%;
|
||||
top: 0;
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.heroCard:hover .heroFurniture,
|
||||
.heroCard:focus-within .heroFurniture {
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
.heroCard:hover .heroNav img,
|
||||
.heroCard:focus-within .heroNav img {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.heroIceCream {
|
||||
width: 84%;
|
||||
height: auto;
|
||||
left: 8%;
|
||||
top: 14%;
|
||||
}
|
||||
|
||||
.heroRug {
|
||||
width: 81%;
|
||||
height: auto;
|
||||
left: 10%;
|
||||
top: 16%;
|
||||
}
|
||||
|
||||
/* Beat globals `.tile-hover > *:not(img) { position: relative }` so arrows stay overlaid */
|
||||
.heroCard .heroNav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 29px;
|
||||
height: 29px;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.heroCard .heroNav img {
|
||||
width: 11px;
|
||||
height: 22px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.heroCard .heroPrev {
|
||||
left: 16px;
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
}
|
||||
|
||||
.heroCard .heroNext {
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.join {
|
||||
margin-top: 7px;
|
||||
background: var(--color-card);
|
||||
border-radius: 5px;
|
||||
min-height: 136px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 23px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.joinIcon {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.join:hover .joinIcon,
|
||||
.join:focus-within .joinIcon {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.joinCopy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.joinTitle {
|
||||
display: block;
|
||||
font-size: 30px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 6px;
|
||||
text-transform: uppercase;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.joinText {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1.4px;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.sectionHead {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
max-width: var(--container-width);
|
||||
width: 100%;
|
||||
margin: 0 auto 28px;
|
||||
padding: 0 var(--page-pad);
|
||||
}
|
||||
|
||||
.sectionHeadCentered {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 0 0 36px;
|
||||
}
|
||||
|
||||
.arrows {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.arrowBtn {
|
||||
width: 38px;
|
||||
height: 36px;
|
||||
background: var(--color-card);
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.arrowBtn img {
|
||||
width: 8px;
|
||||
height: 14px;
|
||||
object-fit: contain;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.arrowBtn:global(.tile-hover):hover img,
|
||||
.arrowBtn:global(.tile-hover):focus-visible img {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.arrowPrev {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.sectionTitles {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 6px;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sectionSubtitle {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 3.2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.explore {
|
||||
height: 36px;
|
||||
min-width: 180px;
|
||||
background: var(--color-explore);
|
||||
border-radius: 5px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-soft);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.explore img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.explore:global(.tile-hover):hover img,
|
||||
.explore:global(.tile-hover):focus-visible img {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.categoryGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 28px 20px;
|
||||
}
|
||||
|
||||
.categoryCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding-top: 38%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.categoryStage {
|
||||
width: 100%;
|
||||
aspect-ratio: 430 / 187;
|
||||
background: var(--color-card);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 8px 10px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.categoryImage {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 58%;
|
||||
height: 120%;
|
||||
bottom: 36%;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.industry {
|
||||
width: 59%;
|
||||
height: 135%;
|
||||
bottom: 33%;
|
||||
}
|
||||
|
||||
.food {
|
||||
width: 50%;
|
||||
height: 114%;
|
||||
bottom: 45%;
|
||||
}
|
||||
|
||||
.fruits {
|
||||
width: 63%;
|
||||
height: 144%;
|
||||
bottom: 33%;
|
||||
}
|
||||
|
||||
.clothes {
|
||||
width: 58%;
|
||||
height: 148%;
|
||||
bottom: 32%;
|
||||
}
|
||||
|
||||
.chair {
|
||||
width: 43%;
|
||||
height: 137%;
|
||||
bottom: 45%;
|
||||
}
|
||||
|
||||
.shoes {
|
||||
width: 79%;
|
||||
height: 96%;
|
||||
bottom: 44%;
|
||||
}
|
||||
|
||||
.washer {
|
||||
width: 58%;
|
||||
height: 133%;
|
||||
bottom: 44%;
|
||||
}
|
||||
|
||||
.services {
|
||||
width: 69%;
|
||||
height: 159%;
|
||||
bottom: 26%;
|
||||
}
|
||||
|
||||
.categoryTitle {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.categoryCopy {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 300;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.producerGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 36px;
|
||||
align-items: start;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--page-pad);
|
||||
}
|
||||
|
||||
.about {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 32px;
|
||||
align-items: center;
|
||||
padding: 48px 0 8px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.aboutKicker {
|
||||
font-size: 36px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.aboutKicker span {
|
||||
font-size: 24px;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.aboutLead {
|
||||
margin-top: 6px;
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
color: var(--color-soft);
|
||||
}
|
||||
|
||||
.aboutBody {
|
||||
margin-top: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: var(--color-body);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.aboutList {
|
||||
list-style: none;
|
||||
margin-top: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.aboutList li {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
font-size: 15px;
|
||||
color: var(--color-body);
|
||||
}
|
||||
|
||||
.aboutList img {
|
||||
width: 9px;
|
||||
height: 11px;
|
||||
margin-top: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.aboutBtn {
|
||||
margin-top: 22px;
|
||||
height: 34px;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
letter-spacing: 2.4px;
|
||||
}
|
||||
|
||||
.aboutVisual {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 837 / 639;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.aboutFrame {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 58.8%;
|
||||
height: 86%;
|
||||
background: var(--color-card);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.aboutShip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 94.5%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: 55% 35%;
|
||||
}
|
||||
|
||||
.bannerBlock {
|
||||
margin-top: 96px;
|
||||
overflow: visible;
|
||||
padding-bottom: clamp(40px, 7vw, 110px);
|
||||
}
|
||||
|
||||
.bannerRow {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2.92fr) repeat(3, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
align-items: end;
|
||||
padding-top: 96px;
|
||||
}
|
||||
|
||||
.bannerCard {
|
||||
position: relative;
|
||||
background: var(--color-card);
|
||||
border-radius: 5px;
|
||||
min-height: 360px;
|
||||
padding: 24px 18px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
gap: 14px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.bannerWide {
|
||||
min-height: 360px;
|
||||
padding-left: 48px;
|
||||
}
|
||||
|
||||
.bannerImage {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: min(280px, 56%);
|
||||
bottom: 118px;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tv {
|
||||
width: min(320px, 48%);
|
||||
bottom: 130px;
|
||||
left: 42%;
|
||||
}
|
||||
|
||||
.pistachio {
|
||||
width: 130%;
|
||||
max-width: 260px;
|
||||
bottom: 108px;
|
||||
transform: translateX(-50%) rotate(18deg);
|
||||
}
|
||||
|
||||
.shampoo {
|
||||
width: 58%;
|
||||
height: auto;
|
||||
bottom: 115px;
|
||||
}
|
||||
|
||||
.saffron {
|
||||
width: 54%;
|
||||
bottom: 115px;
|
||||
}
|
||||
|
||||
.bannerTitle {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 3.5px;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bannerWide .bannerTitle {
|
||||
font-size: 32px;
|
||||
letter-spacing: 6px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.bannerBtn {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
align-self: flex-start;
|
||||
height: 29px;
|
||||
padding: 0 14px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 2.2px;
|
||||
}
|
||||
|
||||
.bannerBtnWide {
|
||||
height: 34px;
|
||||
padding: 0 18px;
|
||||
font-size: 14px;
|
||||
letter-spacing: 2.4px;
|
||||
}
|
||||
|
||||
.exportCta {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-top: 7px;
|
||||
background: var(--color-card);
|
||||
border-radius: 5px;
|
||||
height: min(201px, calc((min(100vw, var(--max-width)) - 2 * var(--page-pad)) * 201 / 1800));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 0 40px 0 22%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.plane {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: min(220px, 13.5%);
|
||||
height: auto;
|
||||
max-width: none;
|
||||
object-fit: contain;
|
||||
transform: translate(-6%, -58%) rotate(42deg);
|
||||
transform-origin: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.exportTitle {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
font-size: 32px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 6px;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.beginBtn {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 34px;
|
||||
min-width: 320px;
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
letter-spacing: 2.8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.beginBtn img {
|
||||
width: 9px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.exportCta:hover .beginBtn img,
|
||||
.exportCta:focus-within .beginBtn img {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.sectionTitle {
|
||||
letter-spacing: 4px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.heroTitle {
|
||||
font-size: 28px;
|
||||
letter-spacing: 5px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.aboutKicker {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.hero,
|
||||
.bannerRow {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.heroCard {
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.heroMain {
|
||||
padding: 48px 24px 32px;
|
||||
}
|
||||
|
||||
.heroFurniture,
|
||||
.heroIceCream,
|
||||
.heroRug {
|
||||
width: min(280px, 46%);
|
||||
}
|
||||
|
||||
.categoryGrid,
|
||||
.producerGrid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.about {
|
||||
grid-template-columns: 1fr;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.aboutVisual {
|
||||
min-height: unset;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.sectionHead {
|
||||
grid-template-columns: 1fr;
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.exportCta {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 24px 20px 24px;
|
||||
height: auto;
|
||||
min-height: 140px;
|
||||
}
|
||||
|
||||
.plane {
|
||||
left: 50%;
|
||||
top: -8%;
|
||||
width: min(180px, 42%);
|
||||
transform: translate(-50%, -40%) rotate(42deg);
|
||||
}
|
||||
|
||||
.beginBtn {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.heroCard {
|
||||
min-height: 360px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.heroMain {
|
||||
padding: 32px 20px 24px;
|
||||
}
|
||||
|
||||
.heroTitle,
|
||||
.joinTitle,
|
||||
.aboutKicker {
|
||||
font-size: 24px;
|
||||
letter-spacing: 3px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 15px;
|
||||
letter-spacing: 2.5px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.heroFurniture,
|
||||
.heroIceCream,
|
||||
.heroRug {
|
||||
width: 42%;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.join {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.joinCopy {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.categoryGrid,
|
||||
.producerGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.explore {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.categoryCard {
|
||||
padding-top: 28%;
|
||||
max-width: 280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Montserrat } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const montserrat = Montserrat({
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "500", "600"],
|
||||
variable: "--font-montserrat",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "IranBay | A gate to iran market",
|
||||
description: "IranBay homepage — a gate to Iran’s marketplace.",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
return (
|
||||
<html lang="en" dir="ltr" className={montserrat.variable}>
|
||||
<body className={montserrat.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import ProducerCard from "@/components/ProducerCard";
|
||||
import {
|
||||
aboutBullets,
|
||||
featuredCategories,
|
||||
featuredProducers,
|
||||
type Producer,
|
||||
} from "@/data/home";
|
||||
import {
|
||||
getWebsiteHomeMedia,
|
||||
type WebsiteMediaItem,
|
||||
} from "@/lib/website-media";
|
||||
import {
|
||||
carouselBusinessToProducer,
|
||||
getWebsiteCarousels,
|
||||
type WebsiteCarousel,
|
||||
} from "@/lib/website-carousels";
|
||||
import styles from "./home.module.css";
|
||||
|
||||
function SectionHeading({
|
||||
title,
|
||||
subtitle,
|
||||
exploreHref,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
exploreHref: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.sectionHead}>
|
||||
<div className={styles.arrows}>
|
||||
<button
|
||||
className={`tile-hover ${styles.arrowBtn}`}
|
||||
type="button"
|
||||
aria-label="Previous"
|
||||
>
|
||||
<img
|
||||
className={styles.arrowPrev}
|
||||
src="/images/icons/chevron.svg"
|
||||
alt=""
|
||||
width={8}
|
||||
height={14}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className={`tile-hover ${styles.arrowBtn}`}
|
||||
type="button"
|
||||
aria-label="Next"
|
||||
>
|
||||
<img src="/images/icons/chevron.svg" alt="" width={8} height={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.sectionTitles}>
|
||||
<h2 className={styles.sectionTitle}>{title}</h2>
|
||||
<p className={styles.sectionSubtitle}>
|
||||
{subtitle || "some of our most crowded categories"}
|
||||
</p>
|
||||
</div>
|
||||
<a className={`tile-hover ${styles.explore}`} href={exploreHref}>
|
||||
Explore all
|
||||
<img src="/images/icons/menu.svg" alt="" width={22} height={22} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BusinessCarouselSection({
|
||||
carousel,
|
||||
fallbackProducers,
|
||||
}: {
|
||||
carousel?: WebsiteCarousel;
|
||||
fallbackProducers?: Producer[];
|
||||
}) {
|
||||
if (!carousel) return null;
|
||||
|
||||
const producers =
|
||||
carousel.businesses.length > 0
|
||||
? carousel.businesses.map(carouselBusinessToProducer)
|
||||
: (fallbackProducers ?? []);
|
||||
|
||||
if (!producers.length) return null;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`${styles.section} ${styles.carousel}`}
|
||||
id={carousel.slug || carousel.id}
|
||||
>
|
||||
<SectionHeading
|
||||
title={carousel.title}
|
||||
subtitle={carousel.subtext || undefined}
|
||||
exploreHref="/businesses"
|
||||
/>
|
||||
<div className={styles.producerGrid}>
|
||||
{producers.map((producer) => (
|
||||
<ProducerCard
|
||||
key={`${carousel.id}-${producer.slug}`}
|
||||
producer={producer}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function TitleLines({ text }: { text: string }) {
|
||||
const lines = text.includes("\n")
|
||||
? text.split("\n")
|
||||
: (() => {
|
||||
const parts = text.trim().split(/\s+/);
|
||||
if (parts.length <= 1) return [text];
|
||||
return [parts[0], parts.slice(1).join(" ")];
|
||||
})();
|
||||
|
||||
return (
|
||||
<>
|
||||
{lines.map((line, index) => (
|
||||
<span key={`${line}-${index}`}>
|
||||
{index > 0 ? <br /> : null}
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function fallbackMedia() {
|
||||
return {
|
||||
slider: [
|
||||
{
|
||||
id: "slider-1",
|
||||
imageUrl: "/images/hero/furniture.png",
|
||||
linkUrl: "#producers",
|
||||
title: "Iranian modern art",
|
||||
cta: "browse furniture producers",
|
||||
},
|
||||
] satisfies WebsiteMediaItem[],
|
||||
standingBanners: [
|
||||
{
|
||||
id: "standing-1",
|
||||
imageUrl: "/images/hero/ice-cream.png",
|
||||
linkUrl: "#producers",
|
||||
title: "food industry",
|
||||
cta: "browse producers",
|
||||
},
|
||||
{
|
||||
id: "standing-2",
|
||||
imageUrl: "/images/hero/rug.png",
|
||||
linkUrl: "#producers",
|
||||
title: "original & pure art",
|
||||
cta: "browse producers",
|
||||
},
|
||||
] satisfies WebsiteMediaItem[],
|
||||
categories: featuredCategories.map((category, index) => ({
|
||||
id: `cat-${index + 1}`,
|
||||
imageUrl: category.image,
|
||||
linkUrl: "#producers",
|
||||
title: category.title,
|
||||
subtitle: category.description,
|
||||
})),
|
||||
menuImages: [],
|
||||
aboutUs: {
|
||||
id: "about-1",
|
||||
imageUrl: "/images/banners/ship.png",
|
||||
linkUrl: "/about",
|
||||
} satisfies WebsiteMediaItem,
|
||||
banners: [
|
||||
{
|
||||
id: "banner-1",
|
||||
imageUrl: "/images/banners/tv.png",
|
||||
linkUrl: "#producers",
|
||||
title: "Iranian modern art",
|
||||
cta: "browse furniture producers",
|
||||
},
|
||||
{
|
||||
id: "banner-2",
|
||||
imageUrl: "/images/banners/pistachio.png",
|
||||
linkUrl: "#producers",
|
||||
title: "food industry",
|
||||
cta: "browse producers",
|
||||
},
|
||||
{
|
||||
id: "banner-3",
|
||||
imageUrl: "/images/banners/shampoo.png",
|
||||
linkUrl: "#producers",
|
||||
title: "food industry",
|
||||
cta: "browse producers",
|
||||
},
|
||||
{
|
||||
id: "banner-4",
|
||||
imageUrl: "/images/banners/saffron.png",
|
||||
linkUrl: "#producers",
|
||||
title: "original & pure art",
|
||||
cta: "browse producers",
|
||||
},
|
||||
{
|
||||
id: "banner-5",
|
||||
imageUrl: "/images/banners/plane.png",
|
||||
linkUrl: "/register",
|
||||
title: "let's export",
|
||||
cta: "begin",
|
||||
},
|
||||
] satisfies WebsiteMediaItem[],
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Home() {
|
||||
const [mediaResult, carousels] = await Promise.all([
|
||||
getWebsiteHomeMedia(),
|
||||
getWebsiteCarousels(),
|
||||
]);
|
||||
const media = mediaResult ?? fallbackMedia();
|
||||
const slide = media.slider[0];
|
||||
const standing = media.standingBanners;
|
||||
const collectionBanners = media.banners.slice(0, 4);
|
||||
const exportBanner =
|
||||
media.banners[4] ?? media.banners[media.banners.length - 1];
|
||||
|
||||
const [firstCarousel, secondCarousel, ...restCarousels] = carousels;
|
||||
|
||||
return (
|
||||
<div className={styles.page} id="top">
|
||||
<Header />
|
||||
|
||||
<main>
|
||||
<section className={styles.section} aria-label="Featured collections">
|
||||
<div className={styles.hero}>
|
||||
<article className={`tile-hover ${styles.heroCard} ${styles.heroMain}`}>
|
||||
<button
|
||||
className={`${styles.heroNav} ${styles.heroPrev}`}
|
||||
type="button"
|
||||
aria-label="Previous slide"
|
||||
>
|
||||
<img src="/images/icons/chevron.svg" alt="" width={11} height={22} />
|
||||
</button>
|
||||
<div className={styles.heroCopy}>
|
||||
<h1 className={styles.heroTitle}>
|
||||
<TitleLines text={slide?.title || "Iranian modern art"} />
|
||||
</h1>
|
||||
<a
|
||||
className={`outline-btn ${styles.heroBtn}`}
|
||||
href={slide?.linkUrl || "#producers"}
|
||||
>
|
||||
{slide?.cta || "browse furniture producers"}
|
||||
</a>
|
||||
</div>
|
||||
<img
|
||||
className={`${styles.heroImage} ${styles.heroFurniture}`}
|
||||
src={slide?.imageUrl || "/images/hero/furniture.png"}
|
||||
alt={slide?.title || "Iranian modern furniture"}
|
||||
/>
|
||||
<button
|
||||
className={`${styles.heroNav} ${styles.heroNext}`}
|
||||
type="button"
|
||||
aria-label="Next slide"
|
||||
>
|
||||
<img src="/images/icons/chevron.svg" alt="" width={11} height={22} />
|
||||
</button>
|
||||
</article>
|
||||
|
||||
{standing.slice(0, 2).map((banner, index) => (
|
||||
<article key={banner.id} className={`tile-hover ${styles.heroCard}`}>
|
||||
<img
|
||||
className={`${styles.heroImage} ${
|
||||
index === 0 ? styles.heroIceCream : styles.heroRug
|
||||
}`}
|
||||
src={banner.imageUrl}
|
||||
alt={banner.title || ""}
|
||||
/>
|
||||
<h2 className={`${styles.heroTitle} ${styles.heroTitleSmall}`}>
|
||||
<TitleLines text={banner.title || "Browse"} />
|
||||
</h2>
|
||||
<a
|
||||
className={`outline-btn ${styles.heroBtnSmall}`}
|
||||
href={banner.linkUrl || "#producers"}
|
||||
>
|
||||
{banner.cta || "browse producers"}
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={styles.section} id="join">
|
||||
<a href="/register" className={`tile-hover ${styles.join}`}>
|
||||
<img
|
||||
className={styles.joinIcon}
|
||||
src="/images/icons/join-add.svg"
|
||||
alt=""
|
||||
width={62}
|
||||
height={62}
|
||||
/>
|
||||
<span className={styles.joinCopy}>
|
||||
<span className={styles.joinTitle}>Join our club</span>
|
||||
<span className={styles.joinText}>
|
||||
we will show your business to verity
|
||||
<br />
|
||||
of visitor around the world
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<section className={`${styles.section} ${styles.container}`} id="categories">
|
||||
<div className={styles.sectionHeadCentered}>
|
||||
<h2 className={styles.sectionTitle}>Featured categories</h2>
|
||||
<p className={styles.sectionSubtitle}>
|
||||
some of our most crowded categories
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.categoryGrid}>
|
||||
{media.categories.map((category) => (
|
||||
<a
|
||||
key={category.id}
|
||||
href={category.linkUrl || "#producers"}
|
||||
className={styles.categoryCard}
|
||||
>
|
||||
<div className={`tile-hover ${styles.categoryStage}`}>
|
||||
<img
|
||||
className={styles.categoryImage}
|
||||
src={category.imageUrl}
|
||||
alt=""
|
||||
/>
|
||||
<h3 className={styles.categoryTitle}>
|
||||
{category.title || "Category"}
|
||||
</h3>
|
||||
<p className={styles.categoryCopy}>
|
||||
{category.subtitle || ""}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<BusinessCarouselSection
|
||||
carousel={firstCarousel}
|
||||
fallbackProducers={featuredProducers}
|
||||
/>
|
||||
|
||||
<section className={`${styles.section} ${styles.container}`} id="about">
|
||||
<div className={styles.about}>
|
||||
<div>
|
||||
<h2 className={styles.aboutKicker}>
|
||||
IranBay<span>,</span>
|
||||
</h2>
|
||||
<p className={styles.aboutLead}>
|
||||
We are a gate to iran’s market place
|
||||
</p>
|
||||
<p className={styles.aboutBody}>
|
||||
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. Duis aute irure dolor in
|
||||
reprehenderit in voluptate velit esse cillum dolore eu fugiat
|
||||
nulla pariatur. Excepteur sint occaecat cupidatat non proident,
|
||||
sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
</p>
|
||||
<ul className={styles.aboutList}>
|
||||
{aboutBullets.map((item, index) => (
|
||||
<li key={`${item}-${index}`}>
|
||||
<img src="/images/icons/bullet.svg" alt="" width={11} height={11} />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<a
|
||||
className={`outline-btn ${styles.aboutBtn}`}
|
||||
href={media.aboutUs.linkUrl || "/about"}
|
||||
>
|
||||
know more about us
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.aboutVisual}>
|
||||
<div className={styles.aboutFrame} />
|
||||
<img
|
||||
className={styles.aboutShip}
|
||||
src={media.aboutUs.imageUrl}
|
||||
alt="Cargo ship entering the market"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<BusinessCarouselSection
|
||||
carousel={secondCarousel}
|
||||
fallbackProducers={featuredProducers}
|
||||
/>
|
||||
|
||||
<section className={`${styles.section} ${styles.bannerBlock}`} aria-label="Browse collections">
|
||||
<div className={styles.bannerRow}>
|
||||
{collectionBanners.map((banner, index) => (
|
||||
<article
|
||||
key={banner.id}
|
||||
className={`tile-hover ${styles.bannerCard} ${
|
||||
index === 0 ? styles.bannerWide : ""
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
className={styles.bannerImage}
|
||||
src={banner.imageUrl}
|
||||
alt={banner.title || ""}
|
||||
/>
|
||||
<h2 className={styles.bannerTitle}>
|
||||
<TitleLines text={banner.title || "Browse"} />
|
||||
</h2>
|
||||
<a
|
||||
className={`outline-btn ${styles.bannerBtn} ${
|
||||
index === 0 ? styles.bannerBtnWide : ""
|
||||
}`}
|
||||
href={banner.linkUrl || "#producers"}
|
||||
>
|
||||
{banner.cta || "browse producers"}
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
{exportBanner ? (
|
||||
<div className={`tile-hover ${styles.exportCta}`}>
|
||||
<img
|
||||
className={styles.plane}
|
||||
src={exportBanner.imageUrl}
|
||||
alt=""
|
||||
/>
|
||||
<h2 className={styles.exportTitle}>
|
||||
<TitleLines text={exportBanner.title || "let's export"} />
|
||||
</h2>
|
||||
<a
|
||||
className={`outline-btn ${styles.beginBtn}`}
|
||||
href={exportBanner.linkUrl || "/register"}
|
||||
>
|
||||
{exportBanner.cta || "begin"}
|
||||
<img src="/images/icons/chevron.svg" alt="" width={9} height={16} />
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
{restCarousels.map((carousel, index) => (
|
||||
<BusinessCarouselSection
|
||||
key={carousel.id}
|
||||
carousel={carousel}
|
||||
fallbackProducers={index === 0 ? featuredProducers : undefined}
|
||||
/>
|
||||
))}
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.page {
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
.shell {
|
||||
max-width: 1920px;
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--page-pad);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import BusinessRegistration from "@/components/BusinessRegistration";
|
||||
import RequireAuth from "@/components/RequireAuth";
|
||||
import styles from "./page.module.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "Business registration | IranBay",
|
||||
description: "Register your business on IranBay in three steps.",
|
||||
};
|
||||
|
||||
export default function RegisterPage() {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<Header />
|
||||
<main className={styles.shell}>
|
||||
<RequireAuth returnPath="/register">
|
||||
<BusinessRegistration />
|
||||
</RequireAuth>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
.page {
|
||||
background: var(--color-white);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: var(--container-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--page-pad);
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 26px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 10px;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sectionSubtitle {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 3.2px;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
color: var(--color-soft);
|
||||
}
|
||||
|
||||
.outlineBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 36px;
|
||||
padding: 0 18px;
|
||||
border: 0.5px solid var(--color-black);
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 2.4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pageBanner {
|
||||
background: var(--color-card);
|
||||
padding: 48px var(--page-pad);
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.pageBannerTitle {
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pageBannerSub {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-soft);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
font-size: 17px;
|
||||
font-weight: 300;
|
||||
line-height: 26px;
|
||||
color: #9d9d9d;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.body a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.quote {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { BlogPost } from "@/data/details";
|
||||
import styles from "./ArticleBody.module.css";
|
||||
|
||||
export default function ArticleBody({
|
||||
blocks,
|
||||
}: {
|
||||
blocks: BlogPost["body"];
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
{blocks.map((block, index) => {
|
||||
if (block.type === "quote") {
|
||||
return (
|
||||
<p key={index} className={styles.quote}>
|
||||
{block.text}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (block.type === "rich") {
|
||||
return (
|
||||
<p key={index}>
|
||||
{block.text}
|
||||
{block.linkHref ? (
|
||||
<a href={block.linkHref} target="_blank" rel="noreferrer">
|
||||
{block.linkText}
|
||||
</a>
|
||||
) : null}
|
||||
{block.afterLink}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return <p key={index}>{block.text}</p>;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,806 @@
|
||||
.root {
|
||||
width: 100%;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
margin-top: 28px;
|
||||
height: clamp(220px, 28vw, 408px);
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.heroImg {
|
||||
object-fit: cover;
|
||||
object-position: center 35%;
|
||||
}
|
||||
|
||||
.profile {
|
||||
margin-top: -40px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 0 clamp(12px, 4vw, 72px);
|
||||
}
|
||||
|
||||
.profileBar {
|
||||
background: #f6f6f6;
|
||||
border-radius: 5px;
|
||||
min-height: 121px;
|
||||
padding: 18px 22px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: clamp(16px, 1.4vw, 24px);
|
||||
font-weight: 400;
|
||||
letter-spacing: 4.8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
margin-top: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: clamp(20px, 3vw, 48px);
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.statWide {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #969696;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.statLabelStack {
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
text-transform: uppercase;
|
||||
color: #969696;
|
||||
}
|
||||
|
||||
.statValueSm {
|
||||
font-size: clamp(20px, 2vw, 30px);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.statValueLg {
|
||||
font-size: clamp(32px, 3vw, 50px);
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 24px;
|
||||
max-width: 1568px;
|
||||
font-size: clamp(13px, 1vw, 16px);
|
||||
line-height: 1.55;
|
||||
color: var(--color-body);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
margin-top: 36px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: clamp(20px, 2.5vw, 40px);
|
||||
border-bottom: 1px solid #ececec;
|
||||
padding: 0 clamp(12px, 4vw, 40px);
|
||||
}
|
||||
|
||||
.tab,
|
||||
.tabActive {
|
||||
position: relative;
|
||||
padding: 0 6px 14px;
|
||||
font-size: clamp(12px, 1vw, 15px);
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-soft);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.tabActive {
|
||||
font-weight: 500;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.tabActive::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -1px;
|
||||
height: 3px;
|
||||
background: var(--color-black);
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-top: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.filtersBar {
|
||||
background: transparent;
|
||||
border: 1px solid #eee;
|
||||
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;
|
||||
}
|
||||
|
||||
.filtersLabel strong {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 6px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.filtersLabel span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #9c9c9c;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.filterField {
|
||||
position: relative;
|
||||
min-width: min(100%, 280px);
|
||||
flex: 1 1 240px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
min-height: 44px;
|
||||
padding: 14px 14px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.filterField span {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 12px;
|
||||
background: #fff;
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
color: var(--color-body);
|
||||
}
|
||||
|
||||
.filterField input {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filterBtn {
|
||||
min-height: 45px;
|
||||
padding: 0 28px;
|
||||
font-size: 20px;
|
||||
letter-spacing: 4px;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-soft);
|
||||
}
|
||||
|
||||
.filterBtn img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
opacity: 0.55;
|
||||
transform: rotate(90deg);
|
||||
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 {
|
||||
opacity: 1;
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.activeTags {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.activeTag {
|
||||
min-height: 41px;
|
||||
padding: 0 14px;
|
||||
font-size: 16px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.activeTag em {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
color: #999;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.activeTag button {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
color: #999;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.filter-chip).activeTag:hover,
|
||||
:global(.filter-chip).activeTag:focus-within {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
:global(.filter-chip).activeTag:hover em,
|
||||
:global(.filter-chip).activeTag:hover button,
|
||||
:global(.filter-chip).activeTag:focus-within em,
|
||||
:global(.filter-chip).activeTag:focus-within button {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.productGrid,
|
||||
.blogGrid {
|
||||
margin-top: 28px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.productCard,
|
||||
.blogCard {
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
|
||||
.productCard:hover,
|
||||
.blogCard:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.productMedia,
|
||||
.blogMedia {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
margin: 12px 12px 0;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.productImg,
|
||||
.blogImg {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.blogImg {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.productBody,
|
||||
.blogBody {
|
||||
padding: 14px 12px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.productBody h3,
|
||||
.blogBody h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.stars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.starFilled {
|
||||
color: #111;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.starEmpty {
|
||||
color: #d0d0d0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rating {
|
||||
margin-left: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
color: #969696;
|
||||
}
|
||||
|
||||
.productMeta {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 23px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 5px;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.chip strong {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.chip em {
|
||||
font-style: normal;
|
||||
font-size: 9px;
|
||||
line-height: 8px;
|
||||
text-transform: uppercase;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.blogBody p {
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
line-height: 18px;
|
||||
color: #9d9d9d;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.blogMeta {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
color: var(--color-soft);
|
||||
}
|
||||
|
||||
.blogMeta span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.blogMeta img {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.contact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.contactRows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.contactRow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.contactIcon {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 22px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.contactLabel {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.contactRow p {
|
||||
font-size: clamp(16px, 1.4vw, 24px);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.socials {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.socialChip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: min(100%, 280px);
|
||||
min-height: 41px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
font-size: 15px;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.socialChip:hover {
|
||||
border-color: #bbb;
|
||||
}
|
||||
|
||||
.socialChip em {
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.map {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: clamp(280px, 40vw, 660px);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.mapImg {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
grid-auto-rows: 120px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.galleryItem {
|
||||
position: relative;
|
||||
grid-row: span 3;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.galleryTall {
|
||||
grid-row: span 5;
|
||||
}
|
||||
|
||||
.galleryMedium {
|
||||
grid-row: span 4;
|
||||
}
|
||||
|
||||
.galleryImg {
|
||||
object-fit: cover;
|
||||
transition: transform 0.35s ease;
|
||||
}
|
||||
|
||||
.galleryItem:hover .galleryImg {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.galleryZoom {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.galleryItem:hover .galleryZoom {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.galleryZoom img {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.portfolioGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.portfolioCard {
|
||||
border: 1px solid #e4e4e4;
|
||||
border-radius: 5px;
|
||||
padding: 28px;
|
||||
background: #fff;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.portfolioMedia {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.portfolioImg {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.portfolioOverlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 28px 24px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
transform: translateY(12px);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
.portfolioOverlayVisible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.portfolioOverlay h3 {
|
||||
font-size: 23px;
|
||||
font-weight: 300;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.portfolioOverlay p {
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
line-height: 18px;
|
||||
color: #9d9d9d;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.productGrid,
|
||||
.blogGrid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.gallery {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.portfolioGrid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.profile {
|
||||
margin-top: -24px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.profileBar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.filtersBar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.filters {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.filterBtn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
justify-content: flex-start;
|
||||
gap: 24px;
|
||||
overflow-x: auto;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.productGrid,
|
||||
.blogGrid,
|
||||
.portfolioGrid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.gallery {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-auto-rows: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.bannerTitle {
|
||||
letter-spacing: 6px;
|
||||
}
|
||||
|
||||
.productGrid,
|
||||
.blogGrid,
|
||||
.portfolioGrid,
|
||||
.gallery {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.portfolioCard {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.socialChip {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,644 @@
|
||||
.root {
|
||||
width: 100%;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: var(--container-width);
|
||||
margin-inline: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.steps {
|
||||
margin-top: 36px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
border-bottom: 1px solid #ececec;
|
||||
}
|
||||
|
||||
.step,
|
||||
.stepActive {
|
||||
position: relative;
|
||||
padding: 0 12px 16px;
|
||||
font-size: clamp(12px, 1.1vw, 15px);
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--color-soft);
|
||||
text-align: center;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.stepActive {
|
||||
font-weight: 500;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.stepActive::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -1px;
|
||||
height: 3px;
|
||||
background: var(--color-black);
|
||||
}
|
||||
|
||||
.step:hover {
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.layout {
|
||||
margin-top: 28px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 158px) minmax(0, 1fr);
|
||||
gap: 28px;
|
||||
align-items: start;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.side {
|
||||
position: relative;
|
||||
min-height: 520px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sideRail {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
width: 100%;
|
||||
max-width: 158px;
|
||||
min-height: 644px;
|
||||
background: #f6f6f6;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.sideImage {
|
||||
position: absolute;
|
||||
left: -64px;
|
||||
top: 140px;
|
||||
z-index: 1;
|
||||
width: 240px;
|
||||
max-width: none;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sideImageLaptop {
|
||||
width: 288px; /* +20% */
|
||||
}
|
||||
|
||||
.sideImagePainting {
|
||||
width: 400px;
|
||||
left: -120px;
|
||||
top: 100px;
|
||||
}
|
||||
|
||||
.sideImageNotebook {
|
||||
width: 380px;
|
||||
left: -110px;
|
||||
top: 90px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stepPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
.typeRow {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.typeIntro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.typeIntro strong {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.54px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.typeIntro span {
|
||||
font-size: 12px;
|
||||
color: #9c9c9c;
|
||||
}
|
||||
|
||||
.typeCards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.typeCard,
|
||||
.typeCardActive {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 56px;
|
||||
padding: 8px 14px 8px 10px;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
background: #f6f6f6;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.typeCard::before,
|
||||
.typeCardActive::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background: var(--color-black);
|
||||
clip-path: inset(100% 0 0 0);
|
||||
transition: clip-path 0.45s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
pointer-events: none;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.typeCard:hover::before,
|
||||
.typeCardActive::before {
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
|
||||
.typeCard:hover,
|
||||
.typeCardActive {
|
||||
color: var(--color-white);
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.typeCardActive {
|
||||
background: #111;
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.typeCard img,
|
||||
.typeCardActive img,
|
||||
.typeCard span,
|
||||
.typeCardActive span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.typeIcon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.typeIcon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
transition: filter 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* Optical scale — source PNGs have uneven padding / visual weight */
|
||||
.typeIconStore {
|
||||
transform: scale(0.88);
|
||||
}
|
||||
|
||||
.typeIconCorporates {
|
||||
transform: scale(1.45);
|
||||
/* Asset is white-on-black; invert so it reads as black outline on grey cards */
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.typeIconIndustrial {
|
||||
transform: scale(1.75);
|
||||
}
|
||||
|
||||
.typeCard:hover .typeIconStore,
|
||||
.typeCard:hover .typeIconIndustrial,
|
||||
.typeCardActive .typeIconStore,
|
||||
.typeCardActive .typeIconIndustrial {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
|
||||
.typeCard:hover .typeIconCorporates,
|
||||
.typeCardActive .typeIconCorporates {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.typeCard span,
|
||||
.typeCardActive span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.typeCard em,
|
||||
.typeCardActive em {
|
||||
font-style: normal;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.typeCard small,
|
||||
.typeCardActive small {
|
||||
font-size: 10px;
|
||||
color: #989898;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.typeCard:hover small,
|
||||
.typeCardActive small {
|
||||
color: #cfcfcf;
|
||||
}
|
||||
|
||||
.row,
|
||||
.row4,
|
||||
.rowLocation {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.row {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.row4 {
|
||||
grid-template-columns: 1.4fr 1fr 0.7fr 1.4fr;
|
||||
}
|
||||
|
||||
.rowLocation {
|
||||
grid-template-columns: 0.9fr 1.2fr 0.8fr;
|
||||
}
|
||||
|
||||
.field {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
min-height: 46px;
|
||||
padding: 14px 14px 10px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
transition: border-color 0.25s ease;
|
||||
}
|
||||
|
||||
.field span {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 12px;
|
||||
background: #fff;
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
color: var(--color-body);
|
||||
transition: color 0.25s ease;
|
||||
}
|
||||
|
||||
.field:focus-within {
|
||||
border-color: var(--color-black);
|
||||
}
|
||||
|
||||
.field:focus-within span {
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field select,
|
||||
.field textarea {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
width: 100%;
|
||||
color: var(--color-black);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.field input::placeholder {
|
||||
color: #c8c8c8;
|
||||
}
|
||||
|
||||
.selectField {
|
||||
padding-right: 34px;
|
||||
}
|
||||
|
||||
.selectField select {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectChevron {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
bottom: 16px;
|
||||
width: 10px;
|
||||
height: 12px;
|
||||
opacity: 0.5;
|
||||
transform: rotate(90deg);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.selectField:focus-within .selectChevron {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.textareaField {
|
||||
min-height: 110px;
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.rule {
|
||||
border: 0;
|
||||
border-top: 1px solid #ececec;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.uploadGrid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 0.4fr) minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.uploadCard,
|
||||
.uploadCardWide {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.uploadHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
min-height: 44px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 5px;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.uploadHeader img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.uploadHeader span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.uploadHeader em {
|
||||
font-style: normal;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.uploadHeader small {
|
||||
font-size: 10px;
|
||||
color: #686868;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.uploadBox {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 220px;
|
||||
border: 1px solid #c1c1c1;
|
||||
border-radius: 5px;
|
||||
font-size: clamp(28px, 3vw, 48px);
|
||||
font-weight: 700;
|
||||
color: #e2e2e2;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.fileInput {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.summarySection {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.summarySection:first-child,
|
||||
.summarySection:nth-of-type(4) {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.summaryItem {
|
||||
position: relative;
|
||||
padding-left: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.summaryItem::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
width: 1px;
|
||||
background: #d7d7d7;
|
||||
}
|
||||
|
||||
.summaryItem span,
|
||||
.summaryBlock span {
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.42px;
|
||||
text-transform: uppercase;
|
||||
color: #9c9c9c;
|
||||
}
|
||||
|
||||
.summaryItem strong {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.summaryBlock {
|
||||
position: relative;
|
||||
padding-left: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.summaryBlock::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
width: 1px;
|
||||
background: #d7d7d7;
|
||||
}
|
||||
|
||||
.summaryBlock p {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
line-height: 1.45;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.footerBar {
|
||||
margin-top: 28px;
|
||||
padding-top: 22px;
|
||||
border-top: 1px solid #ececec;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.backBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 2.4px;
|
||||
text-transform: uppercase;
|
||||
color: #9e9e9e;
|
||||
}
|
||||
|
||||
.backBtn img {
|
||||
width: 8px;
|
||||
height: 10px;
|
||||
opacity: 0.45;
|
||||
transform: scaleX(-1) rotate(0deg);
|
||||
}
|
||||
|
||||
.continueBtn {
|
||||
min-width: min(100%, 240px);
|
||||
min-height: 29px;
|
||||
padding: 0 18px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 2.4px;
|
||||
}
|
||||
|
||||
.confirmHint {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
color: #989898;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.submitError {
|
||||
margin: 12px 0 0;
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.submitOk {
|
||||
margin: 12px 0 0;
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
color: #027a48;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.typeRow {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.row4,
|
||||
.rowLocation,
|
||||
.summarySection,
|
||||
.summarySection:first-child,
|
||||
.summarySection:nth-of-type(4) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.uploadGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.side {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.typeCards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.steps {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.step,
|
||||
.stepActive {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.row,
|
||||
.row4,
|
||||
.rowLocation,
|
||||
.summarySection,
|
||||
.summarySection:first-child,
|
||||
.summarySection:nth-of-type(4) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.footerBar {
|
||||
flex-direction: column-reverse;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.continueBtn,
|
||||
.backBtn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import CategoryMultiSelect from "@/components/CategoryMultiSelect";
|
||||
import DetailBanner from "@/components/DetailBanner";
|
||||
import {
|
||||
categoryLabel,
|
||||
searchMeshkeeCategories,
|
||||
type MeshkeeCategory,
|
||||
} from "@/lib/meshkee-categories";
|
||||
import { registerShop } from "@/lib/marketplace";
|
||||
import styles from "./BusinessRegistration.module.css";
|
||||
|
||||
const steps = [
|
||||
{ id: 1, label: "Business information" },
|
||||
{ id: 2, label: "insert photo" },
|
||||
{ id: 3, label: "Confirm information" },
|
||||
] as const;
|
||||
|
||||
const businessTypes = [
|
||||
{
|
||||
id: "store",
|
||||
title: "Store",
|
||||
subtitle: "In publishing and graphic design",
|
||||
icon: "/images/register/type-store.png",
|
||||
iconClass: "typeIconStore",
|
||||
},
|
||||
{
|
||||
id: "corporates",
|
||||
title: "Corporates",
|
||||
subtitle: "In publishing and graphic design",
|
||||
icon: "/images/register/type-corporates.png",
|
||||
iconClass: "typeIconCorporates",
|
||||
},
|
||||
{
|
||||
id: "industrial",
|
||||
title: "industrial",
|
||||
subtitle: "In publishing and graphic design",
|
||||
icon: "/images/register/type-industrial.png",
|
||||
iconClass: "typeIconIndustrial",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const sideImages = [
|
||||
{
|
||||
src: "/images/register/side-step1.png",
|
||||
className: "sideImageLaptop",
|
||||
},
|
||||
{
|
||||
src: "/images/register/side-step2.png",
|
||||
className: "sideImagePainting",
|
||||
},
|
||||
{
|
||||
src: "/images/register/side-step3.png",
|
||||
className: "sideImageNotebook",
|
||||
},
|
||||
] as const;
|
||||
|
||||
type FormState = {
|
||||
businessType: string;
|
||||
categoryIds: string[];
|
||||
storeName: string;
|
||||
telephone: string;
|
||||
since: string;
|
||||
gmail: string;
|
||||
description: string;
|
||||
country: string;
|
||||
province: string;
|
||||
postalCode: string;
|
||||
address: string;
|
||||
logoName: string;
|
||||
bannerName: string;
|
||||
};
|
||||
|
||||
const initialForm: FormState = {
|
||||
businessType: "corporates",
|
||||
categoryIds: [],
|
||||
storeName: "Sani Khani",
|
||||
telephone: "025 - 3698 52 69",
|
||||
since: "2018",
|
||||
gmail: "@Gmail.com",
|
||||
description: "In publishing and graphic design, Lorem ipsum is a placeholder.",
|
||||
country: "Iran",
|
||||
province: "Qom",
|
||||
postalCode: "03785692",
|
||||
address: "In publishing and graphic design, Lorem ipsum is a placeholder.",
|
||||
logoName: "",
|
||||
bannerName: "",
|
||||
};
|
||||
|
||||
function Field({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
className,
|
||||
type = "text",
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
type?: string;
|
||||
}) {
|
||||
return (
|
||||
<label className={`${styles.field} ${className ?? ""}`}>
|
||||
<span>{label}</span>
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
className,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
options: string[];
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<label className={`${styles.field} ${styles.selectField} ${className ?? ""}`}>
|
||||
<span>{label}</span>
|
||||
<select value={value} onChange={(event) => onChange(event.target.value)}>
|
||||
{options.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<img src="/images/icons/chevron.svg" alt="" className={styles.selectChevron} />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function SummaryItem({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className={styles.summaryItem}>
|
||||
<span>{label}</span>
|
||||
<strong>{value}</strong>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BusinessRegistration() {
|
||||
const [step, setStep] = useState(1);
|
||||
const [form, setForm] = useState<FormState>(initialForm);
|
||||
const [categories, setCategories] = useState<MeshkeeCategory[]>([]);
|
||||
const [categoriesLoading, setCategoriesLoading] = useState(true);
|
||||
const [categoriesError, setCategoriesError] = useState("");
|
||||
const [ownerPassword, setOwnerPassword] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submitError, setSubmitError] = useState("");
|
||||
const [submitOk, setSubmitOk] = useState("");
|
||||
|
||||
const update = <K extends keyof FormState>(key: K, value: FormState[K]) => {
|
||||
setForm((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
async function loadCategories() {
|
||||
setCategoriesLoading(true);
|
||||
setCategoriesError("");
|
||||
try {
|
||||
const items = await searchMeshkeeCategories("", controller.signal);
|
||||
if (controller.signal.aborted) return;
|
||||
setCategories(items);
|
||||
} catch (err) {
|
||||
if (controller.signal.aborted) return;
|
||||
setCategories([]);
|
||||
setCategoriesError(
|
||||
err instanceof Error ? err.message : "Failed to load categories",
|
||||
);
|
||||
} finally {
|
||||
if (!controller.signal.aborted) setCategoriesLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
void loadCategories();
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
|
||||
const goNext = () => setStep((prev) => Math.min(3, prev + 1));
|
||||
const goBack = () => setStep((prev) => Math.max(1, prev - 1));
|
||||
|
||||
const businessTypeLabel =
|
||||
businessTypes.find((item) => item.id === form.businessType)?.title ??
|
||||
form.businessType;
|
||||
|
||||
const selectedCategoryLabels = useMemo(() => {
|
||||
const byId = new Map(categories.map((item) => [item.id, item]));
|
||||
return form.categoryIds
|
||||
.map((id) => byId.get(id))
|
||||
.filter((item): item is MeshkeeCategory => Boolean(item))
|
||||
.map((item) => categoryLabel(item));
|
||||
}, [categories, form.categoryIds]);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<DetailBanner
|
||||
title="Business registration"
|
||||
breadcrumbs={["Home", "Business Register"]}
|
||||
/>
|
||||
|
||||
<div className={styles.container}>
|
||||
<nav className={styles.steps} aria-label="Registration steps">
|
||||
{steps.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={step === item.id ? styles.stepActive : styles.step}
|
||||
onClick={() => setStep(item.id)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className={styles.layout}>
|
||||
<aside className={styles.side} aria-hidden>
|
||||
<div className={styles.sideRail} />
|
||||
<img
|
||||
className={`${styles.sideImage} ${styles[sideImages[step - 1].className]}`}
|
||||
src={sideImages[step - 1].src}
|
||||
alt=""
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<div className={styles.panel}>
|
||||
{step === 1 ? (
|
||||
<div className={styles.stepPanel}>
|
||||
<div className={styles.typeRow}>
|
||||
<div className={styles.typeIntro}>
|
||||
<strong>Category</strong>
|
||||
<span>Do Select</span>
|
||||
</div>
|
||||
<div className={styles.typeCards}>
|
||||
{businessTypes.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={
|
||||
form.businessType === item.id
|
||||
? styles.typeCardActive
|
||||
: styles.typeCard
|
||||
}
|
||||
onClick={() => update("businessType", item.id)}
|
||||
>
|
||||
<span className={styles.typeIcon}>
|
||||
<img
|
||||
className={styles[item.iconClass]}
|
||||
src={item.icon}
|
||||
alt=""
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<em>{item.title}</em>
|
||||
<small>{item.subtitle}</small>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CategoryMultiSelect
|
||||
label="Category"
|
||||
categories={categories}
|
||||
value={form.categoryIds}
|
||||
onChange={(ids) => update("categoryIds", ids)}
|
||||
loading={categoriesLoading}
|
||||
error={categoriesError}
|
||||
/>
|
||||
|
||||
<hr className={styles.rule} />
|
||||
|
||||
<div className={styles.row4}>
|
||||
<Field
|
||||
label="Store name"
|
||||
value={form.storeName}
|
||||
onChange={(value) => update("storeName", value)}
|
||||
/>
|
||||
<Field
|
||||
label="Telephone"
|
||||
value={form.telephone}
|
||||
onChange={(value) => update("telephone", value)}
|
||||
/>
|
||||
<Field
|
||||
label="Since"
|
||||
value={form.since}
|
||||
onChange={(value) => update("since", value)}
|
||||
/>
|
||||
<Field
|
||||
label="Gmail"
|
||||
value={form.gmail}
|
||||
onChange={(value) => update("gmail", value)}
|
||||
placeholder="@Gmail.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className={`${styles.field} ${styles.textareaField}`}>
|
||||
<span>Description</span>
|
||||
<textarea
|
||||
value={form.description}
|
||||
onChange={(event) => update("description", event.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<hr className={styles.rule} />
|
||||
|
||||
<div className={styles.rowLocation}>
|
||||
<SelectField
|
||||
label="country"
|
||||
value={form.country}
|
||||
onChange={(value) => update("country", value)}
|
||||
options={["Iran", "UAE", "Turkey"]}
|
||||
/>
|
||||
<SelectField
|
||||
label="Province"
|
||||
value={form.province}
|
||||
onChange={(value) => update("province", value)}
|
||||
options={["Qom", "Tehran", "Isfahan", "Shiraz"]}
|
||||
/>
|
||||
<Field
|
||||
label="Postal code"
|
||||
value={form.postalCode}
|
||||
onChange={(value) => update("postalCode", value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className={`${styles.field} ${styles.textareaField}`}>
|
||||
<span>Address</span>
|
||||
<textarea
|
||||
value={form.address}
|
||||
onChange={(event) => update("address", event.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{step === 2 ? (
|
||||
<div className={styles.stepPanel}>
|
||||
<div className={styles.uploadGrid}>
|
||||
<label className={styles.uploadCard}>
|
||||
<span className={styles.uploadHeader}>
|
||||
<img src="/images/register/upload.png" alt="" />
|
||||
<span>
|
||||
<em>Enter Logo</em>
|
||||
<small>Max image size: 200kb</small>
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.uploadBox}>
|
||||
{form.logoName ? form.logoName : "111 x 80"}
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className={styles.fileInput}
|
||||
onChange={(event) =>
|
||||
update("logoName", event.target.files?.[0]?.name ?? "")
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className={styles.uploadCardWide}>
|
||||
<span className={styles.uploadHeader}>
|
||||
<img src="/images/register/upload.png" alt="" />
|
||||
<span>
|
||||
<em>Enter Banner</em>
|
||||
<small>Max image size: 2mb</small>
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.uploadBox}>
|
||||
{form.bannerName ? form.bannerName : "1386 x 189"}
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className={styles.fileInput}
|
||||
onChange={(event) =>
|
||||
update("bannerName", event.target.files?.[0]?.name ?? "")
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{step === 3 ? (
|
||||
<div className={styles.stepPanel}>
|
||||
<div className={styles.summarySection}>
|
||||
<SummaryItem label="Business type" value={businessTypeLabel} />
|
||||
<SummaryItem
|
||||
label="Category"
|
||||
value={
|
||||
selectedCategoryLabels.length
|
||||
? selectedCategoryLabels.join(", ")
|
||||
: "—"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<hr className={styles.rule} />
|
||||
<div className={styles.summarySection}>
|
||||
<SummaryItem label="Store Name" value={form.storeName} />
|
||||
<SummaryItem label="Telephone" value={form.telephone} />
|
||||
<SummaryItem label="Since" value={form.since} />
|
||||
<SummaryItem label="Gmail" value={form.gmail} />
|
||||
</div>
|
||||
<hr className={styles.rule} />
|
||||
<div className={styles.summaryBlock}>
|
||||
<span>Discraption</span>
|
||||
<p>{form.description}</p>
|
||||
</div>
|
||||
<hr className={styles.rule} />
|
||||
<div className={styles.summarySection}>
|
||||
<SummaryItem label="Caountry" value={form.country} />
|
||||
<SummaryItem label="province" value={form.province} />
|
||||
<SummaryItem label="postal code" value={form.postalCode} />
|
||||
</div>
|
||||
<hr className={styles.rule} />
|
||||
<div className={styles.summaryBlock}>
|
||||
<span>Address</span>
|
||||
<p>{form.address}</p>
|
||||
</div>
|
||||
<hr className={styles.rule} />
|
||||
<label className={styles.field}>
|
||||
<span>Account password</span>
|
||||
<input
|
||||
type="password"
|
||||
value={ownerPassword}
|
||||
onChange={(event) => setOwnerPassword(event.target.value)}
|
||||
minLength={8}
|
||||
required
|
||||
autoComplete="current-password"
|
||||
placeholder="Confirm your login password"
|
||||
/>
|
||||
</label>
|
||||
<p className={styles.confirmHint}>
|
||||
Confirm your IranBay account password to create your Meshkee
|
||||
shop (separate from the iranbay.com auth tenant).
|
||||
</p>
|
||||
{submitError ? (
|
||||
<p className={styles.submitError} role="alert">
|
||||
{submitError}
|
||||
</p>
|
||||
) : null}
|
||||
{submitOk ? (
|
||||
<p className={styles.submitOk} role="status">
|
||||
{submitOk}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={styles.footerBar}>
|
||||
{step > 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.backBtn}
|
||||
onClick={goBack}
|
||||
disabled={submitting}
|
||||
>
|
||||
<img src="/images/icons/chevron.svg" alt="" />
|
||||
step backward
|
||||
</button>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{step < 3 ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`outline-btn ${styles.continueBtn}`}
|
||||
onClick={goNext}
|
||||
>
|
||||
Continue registration
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={`outline-btn ${styles.continueBtn}`}
|
||||
disabled={submitting || Boolean(submitOk)}
|
||||
onClick={async () => {
|
||||
setSubmitError("");
|
||||
setSubmitOk("");
|
||||
if (ownerPassword.length < 8) {
|
||||
setSubmitError("Enter your account password (min 8 chars).");
|
||||
return;
|
||||
}
|
||||
if (!form.storeName.trim()) {
|
||||
setSubmitError("Store name is required.");
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const result = await registerShop({
|
||||
name: form.storeName.trim(),
|
||||
about: form.description.trim() || undefined,
|
||||
meshkeeCategoryIds: form.categoryIds.length
|
||||
? form.categoryIds
|
||||
: undefined,
|
||||
ownerPassword,
|
||||
});
|
||||
setSubmitOk(
|
||||
result.message ||
|
||||
`Submitted. Meshkee shop #${result.meshkeeBusinessId}`,
|
||||
);
|
||||
} catch (err) {
|
||||
setSubmitError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "Could not register business",
|
||||
);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{submitting ? "Submitting…" : "Confirm information"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
.root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.field {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
min-height: 46px;
|
||||
padding: 14px 34px 10px 14px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
transition: border-color 0.25s ease;
|
||||
}
|
||||
|
||||
.field:focus-within {
|
||||
border-color: var(--color-black);
|
||||
}
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 12px;
|
||||
background: #fff;
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
color: var(--color-body);
|
||||
transition: color 0.25s ease;
|
||||
}
|
||||
|
||||
.field:focus-within .label {
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.control input {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
width: 100%;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.control input::placeholder {
|
||||
color: #c8c8c8;
|
||||
}
|
||||
|
||||
.caret {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
bottom: 16px;
|
||||
width: 10px;
|
||||
height: 12px;
|
||||
opacity: 0.5;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.caret img {
|
||||
width: 10px;
|
||||
height: 12px;
|
||||
transform: rotate(90deg);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.caretOpen img {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
top: calc(100% + 6px);
|
||||
inset-inline: 0;
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.node {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.expand,
|
||||
.expandSpacer {
|
||||
flex: 0 0 22px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.expand {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
color: #989898;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expand:hover,
|
||||
.expand:focus-visible {
|
||||
border-color: var(--color-black);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.option {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px 6px 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: start;
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
color: var(--color-muted);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.option:hover,
|
||||
.option:focus-visible {
|
||||
background: #f7f7f7;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.optionActive {
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.check {
|
||||
flex: 0 0 14px;
|
||||
width: 14px;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.optionLabel {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.empty,
|
||||
.status {
|
||||
margin: 0;
|
||||
padding: 16px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
color: var(--color-body);
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid var(--color-black);
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
color: var(--color-white);
|
||||
background: var(--color-black);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chip:hover,
|
||||
.chip:focus-visible {
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.chip:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
categoryLabel,
|
||||
type MeshkeeCategory,
|
||||
} from "@/lib/meshkee-categories";
|
||||
import styles from "./CategoryMultiSelect.module.css";
|
||||
|
||||
type TreeNode = MeshkeeCategory & { children: TreeNode[] };
|
||||
|
||||
function buildTree(categories: MeshkeeCategory[]): TreeNode[] {
|
||||
const byId = new Map<string, TreeNode>();
|
||||
for (const category of categories) {
|
||||
byId.set(category.id, { ...category, children: [] });
|
||||
}
|
||||
|
||||
const roots: TreeNode[] = [];
|
||||
for (const node of byId.values()) {
|
||||
if (node.parentId && byId.has(node.parentId)) {
|
||||
byId.get(node.parentId)!.children.push(node);
|
||||
} else {
|
||||
roots.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
const sortNodes = (nodes: TreeNode[]) => {
|
||||
nodes.sort(
|
||||
(a, b) =>
|
||||
a.sortOrder - b.sortOrder ||
|
||||
a.name.localeCompare(b.name, undefined, { sensitivity: "base" }),
|
||||
);
|
||||
for (const node of nodes) sortNodes(node.children);
|
||||
};
|
||||
sortNodes(roots);
|
||||
return roots;
|
||||
}
|
||||
|
||||
function matchesQuery(category: MeshkeeCategory, q: string) {
|
||||
if (!q) return true;
|
||||
const hay = [category.name, category.nameFa ?? "", category.slug, ...category.tags]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return hay.includes(q);
|
||||
}
|
||||
|
||||
function filterTree(nodes: TreeNode[], q: string): TreeNode[] {
|
||||
if (!q) return nodes;
|
||||
const result: TreeNode[] = [];
|
||||
for (const node of nodes) {
|
||||
const children = filterTree(node.children, q);
|
||||
if (matchesQuery(node, q) || children.length) {
|
||||
result.push({ ...node, children });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function collectExpandIds(nodes: TreeNode[], into: Set<string>) {
|
||||
for (const node of nodes) {
|
||||
if (node.children.length) {
|
||||
into.add(node.id);
|
||||
collectExpandIds(node.children, into);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Props = {
|
||||
categories: MeshkeeCategory[];
|
||||
value: string[];
|
||||
onChange: (ids: string[]) => void;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
error?: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export default function CategoryMultiSelect({
|
||||
categories,
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
loading = false,
|
||||
error = "",
|
||||
label = "Category",
|
||||
}: Props) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
|
||||
|
||||
const tree = useMemo(() => buildTree(categories), [categories]);
|
||||
const q = query.trim().toLowerCase();
|
||||
const visibleTree = useMemo(() => filterTree(tree, q), [tree, q]);
|
||||
|
||||
const byId = useMemo(() => {
|
||||
const map = new Map<string, MeshkeeCategory>();
|
||||
for (const category of categories) map.set(category.id, category);
|
||||
return map;
|
||||
}, [categories]);
|
||||
|
||||
const selected = value
|
||||
.map((id) => byId.get(id))
|
||||
.filter((item): item is MeshkeeCategory => Boolean(item));
|
||||
|
||||
useEffect(() => {
|
||||
if (!q) return;
|
||||
const next = new Set<string>();
|
||||
collectExpandIds(visibleTree, next);
|
||||
setExpanded(next);
|
||||
}, [q, visibleTree]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function onPointerDown(event: MouseEvent) {
|
||||
if (!rootRef.current?.contains(event.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
function onKey(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") setOpen(false);
|
||||
}
|
||||
document.addEventListener("mousedown", onPointerDown);
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", onPointerDown);
|
||||
document.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
function toggle(id: string) {
|
||||
const next = value.includes(id)
|
||||
? value.filter((item) => item !== id)
|
||||
: [...value, id];
|
||||
onChange(next);
|
||||
}
|
||||
|
||||
function toggleExpand(id: string) {
|
||||
setExpanded((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function renderNodes(nodes: TreeNode[], depth: number) {
|
||||
return nodes.map((node) => {
|
||||
const hasChildren = node.children.length > 0;
|
||||
const isExpanded = q ? true : expanded.has(node.id);
|
||||
const active = value.includes(node.id);
|
||||
|
||||
return (
|
||||
<li key={node.id} className={styles.node}>
|
||||
<div
|
||||
className={styles.row}
|
||||
style={{ paddingInlineStart: `${8 + depth * 16}px` }}
|
||||
>
|
||||
{hasChildren ? (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.expand}
|
||||
aria-label={isExpanded ? "Collapse" : "Expand"}
|
||||
tabIndex={-1}
|
||||
onClick={() => toggleExpand(node.id)}
|
||||
>
|
||||
<span aria-hidden>{isExpanded ? "−" : "+"}</span>
|
||||
</button>
|
||||
) : (
|
||||
<span className={styles.expandSpacer} aria-hidden />
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.option} ${active ? styles.optionActive : ""}`}
|
||||
disabled={disabled}
|
||||
onClick={() => toggle(node.id)}
|
||||
>
|
||||
<span className={styles.check} aria-hidden>
|
||||
{active ? "✓" : ""}
|
||||
</span>
|
||||
<span className={styles.optionLabel}>{categoryLabel(node)}</span>
|
||||
</button>
|
||||
</div>
|
||||
{hasChildren && isExpanded ? (
|
||||
<ul className={styles.list}>{renderNodes(node.children, depth + 1)}</ul>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const placeholder = loading
|
||||
? "Loading categories…"
|
||||
: selected.length
|
||||
? `${selected.length} selected — search to add more`
|
||||
: "Search categories…";
|
||||
|
||||
return (
|
||||
<div className={styles.root} ref={rootRef}>
|
||||
<div className={styles.field}>
|
||||
<span className={styles.label}>{label}</span>
|
||||
<div className={styles.control}>
|
||||
<input
|
||||
value={query}
|
||||
disabled={disabled || loading}
|
||||
placeholder={placeholder}
|
||||
aria-expanded={open}
|
||||
aria-haspopup="listbox"
|
||||
onFocus={() => setOpen(true)}
|
||||
onChange={(event) => {
|
||||
setQuery(event.target.value);
|
||||
setOpen(true);
|
||||
}}
|
||||
onClick={() => setOpen(true)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.caret} ${open ? styles.caretOpen : ""}`}
|
||||
aria-label={open ? "Close categories" : "Open categories"}
|
||||
disabled={disabled || loading}
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
>
|
||||
<img src="/images/icons/chevron.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{open ? (
|
||||
<div className={styles.dropdown} role="listbox" aria-multiselectable>
|
||||
{loading ? (
|
||||
<p className={styles.status}>Loading categories…</p>
|
||||
) : visibleTree.length ? (
|
||||
<ul className={styles.list}>{renderNodes(visibleTree, 0)}</ul>
|
||||
) : (
|
||||
<p className={styles.empty}>
|
||||
{error || "No categories found"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{error && !open ? <p className={styles.error}>{error}</p> : null}
|
||||
|
||||
{selected.length ? (
|
||||
<div className={styles.chips}>
|
||||
{selected.map((category) => (
|
||||
<button
|
||||
key={category.id}
|
||||
type="button"
|
||||
className={styles.chip}
|
||||
disabled={disabled}
|
||||
title="Remove category"
|
||||
onClick={() => toggle(category.id)}
|
||||
>
|
||||
{categoryLabel(category)}
|
||||
<span aria-hidden>×</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { testimonials } from "@/data/pages";
|
||||
import styles from "@/app/about/about.module.css";
|
||||
|
||||
export default function CommentCarousel() {
|
||||
const [index, setIndex] = useState(0);
|
||||
const total = testimonials.length;
|
||||
const item = testimonials[index];
|
||||
|
||||
const prev = () => setIndex((i) => (i - 1 + total) % total);
|
||||
const next = () => setIndex((i) => (i + 1) % total);
|
||||
|
||||
return (
|
||||
<div className={styles.comments}>
|
||||
<div className={styles.commentHead}>
|
||||
<div className={styles.arrows}>
|
||||
<button
|
||||
className={styles.arrowBtn}
|
||||
type="button"
|
||||
aria-label="Previous review"
|
||||
onClick={prev}
|
||||
>
|
||||
<img
|
||||
className={styles.arrowPrev}
|
||||
src="/images/icons/chevron.svg"
|
||||
alt=""
|
||||
width={8}
|
||||
height={14}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className={styles.arrowBtn}
|
||||
type="button"
|
||||
aria-label="Next review"
|
||||
onClick={next}
|
||||
>
|
||||
<img src="/images/icons/chevron.svg" alt="" width={8} height={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.sectionHead}>
|
||||
<h2 className={styles.sectionTitle}>People comment</h2>
|
||||
<p className={styles.sectionSub}>
|
||||
some of our most crowded categories
|
||||
</p>
|
||||
</div>
|
||||
<span className={styles.commentCount} aria-live="polite">
|
||||
{index + 1} / {total}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.commentTrack} key={item.meta}>
|
||||
<p className={styles.quote}>{item.body}</p>
|
||||
<p className={styles.meta}>{item.meta}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.dots} role="tablist" aria-label="Reviews">
|
||||
{testimonials.map((review, i) => (
|
||||
<button
|
||||
key={review.meta}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={i === index}
|
||||
aria-label={`Show review ${i + 1}`}
|
||||
className={i === index ? styles.dotActive : styles.dot}
|
||||
onClick={() => setIndex(i)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { faqs } from "@/data/pages";
|
||||
import styles from "@/app/contact/contact.module.css";
|
||||
|
||||
export default function ContactFaq() {
|
||||
const [open, setOpen] = useState(0);
|
||||
|
||||
return (
|
||||
<div className={styles.faqList}>
|
||||
{faqs.map((item, index) => {
|
||||
const active = open === index;
|
||||
return (
|
||||
<div className={styles.faqItem} key={`${item.question}-${index}`}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.faqTrigger}
|
||||
aria-expanded={active}
|
||||
onClick={() => setOpen(active ? -1 : index)}
|
||||
>
|
||||
<span>{item.question}</span>
|
||||
<img
|
||||
className={active ? styles.faqOpen : styles.faqClosed}
|
||||
src="/images/icons/chevron.svg"
|
||||
alt=""
|
||||
width={10}
|
||||
height={18}
|
||||
/>
|
||||
</button>
|
||||
{active ? <p className={styles.faqAnswer}>{item.answer}</p> : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
.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: 10.5px;
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import styles from "./DetailBanner.module.css";
|
||||
|
||||
export default function DetailBanner({
|
||||
title,
|
||||
breadcrumbs,
|
||||
}: {
|
||||
title: string;
|
||||
breadcrumbs: string[];
|
||||
}) {
|
||||
return (
|
||||
<section className={styles.bannerWrap}>
|
||||
<div className={styles.banner}>
|
||||
<h1 className={styles.bannerTitle}>{title}</h1>
|
||||
<nav className={styles.crumbs} aria-label="Breadcrumb">
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<span key={`${crumb}-${index}`}>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
.footer {
|
||||
background: var(--color-footer);
|
||||
color: var(--color-white);
|
||||
margin-top: 96px;
|
||||
}
|
||||
|
||||
.inner {
|
||||
max-width: var(--container-width);
|
||||
margin: 0 auto;
|
||||
padding: 48px var(--page-pad) 32px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 1fr 1.1fr 0.8fr;
|
||||
gap: 28px 32px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.copy {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 1.35;
|
||||
color: var(--color-footer-text);
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.phones {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.socials {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.socials img {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
margin-top: 32px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--color-footer-line);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 14px;
|
||||
color: var(--color-soft);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.links span {
|
||||
color: var(--color-soft);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.inner {
|
||||
padding: 56px var(--page-pad) 32px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.heading,
|
||||
.copy,
|
||||
.copyright,
|
||||
.links {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import styles from "./Footer.module.css";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className={styles.footer} id="contact">
|
||||
<div className={styles.inner}>
|
||||
<div className={styles.grid}>
|
||||
<div>
|
||||
<h2 className={styles.heading}>logo Here</h2>
|
||||
<p className={styles.copy}>
|
||||
In publishing and graphic design, Lorem ipsum is a placeholder.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className={styles.heading}>Phone Number :</h2>
|
||||
<div className={`${styles.copy} ${styles.phones}`}>
|
||||
<span>+98 937 622 0110</span>
|
||||
<span>+98 912 123 4567</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className={styles.heading}>Location :</h2>
|
||||
<p className={styles.copy}>
|
||||
1 Sheikh Mohammed bin Rashid Blvd - Downtown Dubai - Dubai -
|
||||
United Arab Emirates
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className={styles.heading}>Social Media :</h2>
|
||||
<div className={styles.socials}>
|
||||
<a href="https://instagram.com" aria-label="Instagram">
|
||||
<img src="/images/icons/instagram.png" alt="" width={25} height={25} />
|
||||
</a>
|
||||
<a href="https://t.me" aria-label="Telegram">
|
||||
<img src="/images/icons/telegram.png" alt="" width={26} height={26} />
|
||||
</a>
|
||||
<a href="https://facebook.com" aria-label="Facebook">
|
||||
<img src="/images/icons/facebook.png" alt="" width={20} height={20} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.bottom}>
|
||||
<p className={styles.copyright}>
|
||||
Copy Rights By 2023 IranBay . All Rights Reserved site by Bradly
|
||||
Lancaster.
|
||||
</p>
|
||||
<nav className={styles.links} aria-label="Footer">
|
||||
<a href="/businesses">Categories</a>
|
||||
<span>|</span>
|
||||
<a href="/#vision">Vision</a>
|
||||
<span>|</span>
|
||||
<a href="/about">About us</a>
|
||||
<span>|</span>
|
||||
<a href="/contact">Contact us</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
background: var(--color-white);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.inner {
|
||||
max-width: var(--container-width);
|
||||
margin: 0 auto;
|
||||
padding: 14px var(--page-pad);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.brandText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.brandName {
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 4px;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.brandTag {
|
||||
font-size: 8px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1.6px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22px;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.navLink,
|
||||
.menuTrigger {
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1.6px;
|
||||
text-transform: uppercase;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--color-black);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.homeIcon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.homeIcon img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menuTrigger {
|
||||
background: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.menuTriggerOpen::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 16px;
|
||||
bottom: -14px;
|
||||
height: 1px;
|
||||
background: var(--color-black);
|
||||
}
|
||||
|
||||
.chevron {
|
||||
width: 7px;
|
||||
height: 12px;
|
||||
object-fit: contain;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.headerMega {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.megaHost {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actionsDivider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: #d8d8d8;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.lang {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.langTrigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
line-height: 18px;
|
||||
letter-spacing: 2.4px;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-black);
|
||||
background: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.langChevron {
|
||||
width: 8px;
|
||||
height: 12px;
|
||||
object-fit: contain;
|
||||
transform: rotate(90deg);
|
||||
opacity: 0.7;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.langChevronOpen {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.langMenu {
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
right: 0;
|
||||
z-index: 60;
|
||||
min-width: 72px;
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
list-style: none;
|
||||
background: var(--color-white);
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.langOption,
|
||||
.langOptionActive {
|
||||
width: 100%;
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 2.4px;
|
||||
text-transform: uppercase;
|
||||
text-align: left;
|
||||
color: var(--color-soft);
|
||||
background: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.langOption:hover,
|
||||
.langOptionActive {
|
||||
color: var(--color-black);
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.iconBtn {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.iconBtn img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.iconBtn img.userIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.account {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.accountName {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 140px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-black);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.accountChevron {
|
||||
width: 7px;
|
||||
height: 10px;
|
||||
object-fit: contain;
|
||||
transform: rotate(90deg);
|
||||
opacity: 0.65;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.accountChevronOpen {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.accountTrigger {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 5px;
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.accountMenu {
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
right: 0;
|
||||
z-index: 60;
|
||||
min-width: 196px;
|
||||
padding: 8px 0;
|
||||
background: var(--color-white);
|
||||
border: 1px solid #eee;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.accountItem {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
border: 0;
|
||||
background: none;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
text-align: left;
|
||||
color: var(--color-soft);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.accountItem:hover,
|
||||
.accountItem:focus-visible {
|
||||
color: var(--color-black);
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.accountItemDanger {
|
||||
color: #8e8e8e;
|
||||
}
|
||||
|
||||
.mobileAccount {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--color-card);
|
||||
}
|
||||
|
||||
.mobileAccountName {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.burger {
|
||||
display: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.burger img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.mobileNav {
|
||||
display: none;
|
||||
max-width: var(--container-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.nav,
|
||||
.actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.burger {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.inner {
|
||||
padding: 12px var(--page-pad);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.mobileNav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 8px var(--page-pad) 24px;
|
||||
border-top: 1px solid var(--color-card);
|
||||
}
|
||||
|
||||
.mobileNav a,
|
||||
.mobileNav button {
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 2.4px;
|
||||
text-transform: uppercase;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mobileActions {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.mobileLang {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.mobileLang > span {
|
||||
font-size: 12px;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-soft);
|
||||
}
|
||||
|
||||
.mobileLangOptions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobileLangOptions .langOption,
|
||||
.mobileLangOptions .langOptionActive {
|
||||
width: auto;
|
||||
min-width: 48px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mobileMega {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.mobileMega p {
|
||||
font-size: 12px;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.mobileMega a {
|
||||
display: block;
|
||||
padding: 4px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { megaMenuTabs, navLinks, type MegaTab } from "@/data/home";
|
||||
import {
|
||||
fetchCurrentUser,
|
||||
firstNameFor,
|
||||
getAdminDashboardUrl,
|
||||
getBusinessDashboardUrl,
|
||||
getCustomerDashboardUrl,
|
||||
type WebsiteAuthUser,
|
||||
} from "@/lib/auth-user";
|
||||
import { searchMeshkeeCategories } from "@/lib/meshkee-categories";
|
||||
import { buildMegaMenuTabs } from "@/lib/mega-menu";
|
||||
import { clearTokens, getCustomerLoginUrl } from "@/lib/session";
|
||||
import MegaMenu from "./MegaMenu";
|
||||
import styles from "./Header.module.css";
|
||||
|
||||
const languages = [
|
||||
{ code: "en", label: "en", dir: "ltr" as const },
|
||||
{ code: "ar", label: "ar", dir: "rtl" as const },
|
||||
{ code: "fa", label: "fa", dir: "rtl" as const },
|
||||
];
|
||||
|
||||
function pickDefaultTabId(tabs: MegaTab[]) {
|
||||
return (
|
||||
tabs.find((tab) => tab.id === "stores")?.id ??
|
||||
tabs[0]?.id ??
|
||||
megaMenuTabs[0].id
|
||||
);
|
||||
}
|
||||
|
||||
export default function Header() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [megaOpen, setMegaOpen] = useState(false);
|
||||
const [langOpen, setLangOpen] = useState(false);
|
||||
const [accountOpen, setAccountOpen] = useState(false);
|
||||
const [language, setLanguage] = useState(languages[0]);
|
||||
const [tabs, setTabs] = useState<MegaTab[]>(megaMenuTabs);
|
||||
const [menuImages, setMenuImages] = useState<string[]>([]);
|
||||
const [activeTabId, setActiveTabId] = useState(pickDefaultTabId(megaMenuTabs));
|
||||
const [user, setUser] = useState<WebsiteAuthUser | null>(null);
|
||||
const [authReady, setAuthReady] = useState(false);
|
||||
const closeTimer = useRef<number | undefined>(undefined);
|
||||
const headerRef = useRef<HTMLElement>(null);
|
||||
const langRef = useRef<HTMLDivElement>(null);
|
||||
const accountRef = useRef<HTMLDivElement>(null);
|
||||
const activeTab =
|
||||
tabs.find((tab) => tab.id === activeTabId) ?? tabs[0] ?? megaMenuTabs[0];
|
||||
|
||||
const customerUrl = getCustomerDashboardUrl();
|
||||
const adminUrl = getAdminDashboardUrl();
|
||||
const businessUrl = getBusinessDashboardUrl();
|
||||
const firstName = firstNameFor(user);
|
||||
const isSuperAdmin = user?.dashboard === "super_admin";
|
||||
const ownsBusiness = Boolean(
|
||||
user?.businesses.some((item) => item.isOwner) ||
|
||||
user?.dashboard === "business",
|
||||
);
|
||||
|
||||
const showMega = () => {
|
||||
window.clearTimeout(closeTimer.current);
|
||||
setMegaOpen(true);
|
||||
};
|
||||
|
||||
const hideMega = () => {
|
||||
closeTimer.current = window.setTimeout(() => setMegaOpen(false), 160);
|
||||
};
|
||||
|
||||
const closeMega = () => {
|
||||
window.clearTimeout(closeTimer.current);
|
||||
setMegaOpen(false);
|
||||
};
|
||||
|
||||
const selectLanguage = (code: string) => {
|
||||
const next = languages.find((item) => item.code === code) ?? languages[0];
|
||||
setLanguage(next);
|
||||
setLangOpen(false);
|
||||
document.documentElement.lang = next.code;
|
||||
document.documentElement.dir = next.dir;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
void (async () => {
|
||||
try {
|
||||
const categories = await searchMeshkeeCategories("", controller.signal);
|
||||
if (controller.signal.aborted) return;
|
||||
const nextTabs = buildMegaMenuTabs(categories);
|
||||
if (!nextTabs.length) return;
|
||||
setTabs(nextTabs);
|
||||
setActiveTabId((current) =>
|
||||
nextTabs.some((tab) => tab.id === current)
|
||||
? current
|
||||
: pickDefaultTabId(nextTabs),
|
||||
);
|
||||
} catch {
|
||||
/* keep static fallback */
|
||||
}
|
||||
})();
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
const api =
|
||||
process.env.NEXT_PUBLIC_MARKETPLACE_API_URL?.replace(/\/$/, "") ||
|
||||
"http://localhost:3001/api/v1";
|
||||
void (async () => {
|
||||
try {
|
||||
const response = await fetch(`${api}/website/home-media`, {
|
||||
signal: controller.signal,
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok || controller.signal.aborted) return;
|
||||
const data = (await response.json()) as {
|
||||
menuImages?: { imageUrl?: string }[];
|
||||
};
|
||||
if (controller.signal.aborted) return;
|
||||
setMenuImages(
|
||||
(data.menuImages ?? []).map((item) => item.imageUrl?.trim() ?? ""),
|
||||
);
|
||||
} catch {
|
||||
/* keep built-in menu photos */
|
||||
}
|
||||
})();
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
void (async () => {
|
||||
try {
|
||||
const next = await fetchCurrentUser(controller.signal);
|
||||
if (!controller.signal.aborted) setUser(next);
|
||||
} catch {
|
||||
if (!controller.signal.aborted) setUser(null);
|
||||
} finally {
|
||||
if (!controller.signal.aborted) setAuthReady(true);
|
||||
}
|
||||
})();
|
||||
return () => controller.abort();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
closeMega();
|
||||
setLangOpen(false);
|
||||
setAccountOpen(false);
|
||||
}
|
||||
};
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
const target = event.target as Node;
|
||||
if (!headerRef.current?.contains(target)) {
|
||||
setMegaOpen(false);
|
||||
}
|
||||
if (!langRef.current?.contains(target)) {
|
||||
setLangOpen(false);
|
||||
}
|
||||
if (!accountRef.current?.contains(target)) {
|
||||
setAccountOpen(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
window.addEventListener("pointerdown", onPointerDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKey);
|
||||
window.removeEventListener("pointerdown", onPointerDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
function logout() {
|
||||
clearTokens();
|
||||
setUser(null);
|
||||
setAccountOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<header
|
||||
ref={headerRef}
|
||||
className={`${styles.header} ${megaOpen ? styles.headerMega : ""}`}
|
||||
onMouseLeave={hideMega}
|
||||
>
|
||||
<div className={styles.inner}>
|
||||
<a href="/" className={styles.brand} onMouseEnter={closeMega}>
|
||||
<img
|
||||
className={styles.logo}
|
||||
src="/images/icons/logo.png"
|
||||
alt="IranBay"
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
<span className={styles.brandText}>
|
||||
<span className={styles.brandName}>IranBay</span>
|
||||
<span className={styles.brandTag}>A gate to iran market</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav className={styles.nav} aria-label="Primary">
|
||||
{navLinks.map((link) =>
|
||||
"hasMenu" in link && link.hasMenu ? (
|
||||
<div className={styles.dropdown} key={link.label}>
|
||||
<button
|
||||
className={`${styles.menuTrigger} ${megaOpen ? styles.menuTriggerOpen : ""}`}
|
||||
type="button"
|
||||
aria-expanded={megaOpen}
|
||||
aria-controls="categories-mega"
|
||||
onMouseEnter={showMega}
|
||||
onFocus={showMega}
|
||||
onClick={showMega}
|
||||
>
|
||||
{link.label}
|
||||
<img
|
||||
className={styles.chevron}
|
||||
src="/images/icons/chevron.svg"
|
||||
alt=""
|
||||
width={7}
|
||||
height={12}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className={styles.navLink}
|
||||
onMouseEnter={closeMega}
|
||||
onFocus={closeMega}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
),
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<div className={styles.actions} onMouseEnter={closeMega}>
|
||||
<a href="/register" className={styles.iconBtn} aria-label="Join">
|
||||
<img src="/images/icons/add.svg" alt="" width={30} height={30} />
|
||||
</a>
|
||||
<button className={styles.iconBtn} type="button" aria-label="Search">
|
||||
<img src="/images/icons/search.svg" alt="" width={30} height={30} />
|
||||
</button>
|
||||
|
||||
{authReady && user ? (
|
||||
<div className={styles.account} ref={accountRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.accountName}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={accountOpen}
|
||||
onClick={() => setAccountOpen((value) => !value)}
|
||||
>
|
||||
{firstName}
|
||||
<img
|
||||
className={`${styles.accountChevron} ${accountOpen ? styles.accountChevronOpen : ""}`}
|
||||
src="/images/icons/chevron.svg"
|
||||
alt=""
|
||||
width={7}
|
||||
height={12}
|
||||
/>
|
||||
</button>
|
||||
{accountOpen ? (
|
||||
<div className={styles.accountMenu} role="menu">
|
||||
<a
|
||||
role="menuitem"
|
||||
href={customerUrl}
|
||||
className={styles.accountItem}
|
||||
onClick={() => setAccountOpen(false)}
|
||||
>
|
||||
Customer dashboard
|
||||
</a>
|
||||
<a
|
||||
role="menuitem"
|
||||
href={`${customerUrl}/profile`}
|
||||
className={styles.accountItem}
|
||||
onClick={() => setAccountOpen(false)}
|
||||
>
|
||||
My profile
|
||||
</a>
|
||||
<a
|
||||
role="menuitem"
|
||||
href={`${customerUrl}/forgot-password`}
|
||||
className={styles.accountItem}
|
||||
onClick={() => setAccountOpen(false)}
|
||||
>
|
||||
Change password
|
||||
</a>
|
||||
{ownsBusiness ? (
|
||||
<a
|
||||
role="menuitem"
|
||||
href={businessUrl}
|
||||
className={styles.accountItem}
|
||||
onClick={() => setAccountOpen(false)}
|
||||
>
|
||||
Business dashboard
|
||||
</a>
|
||||
) : null}
|
||||
{isSuperAdmin ? (
|
||||
<a
|
||||
role="menuitem"
|
||||
href={adminUrl}
|
||||
className={styles.accountItem}
|
||||
onClick={() => setAccountOpen(false)}
|
||||
>
|
||||
Super admin
|
||||
</a>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={`${styles.accountItem} ${styles.accountItemDanger}`}
|
||||
onClick={logout}
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<a
|
||||
href={customerUrl}
|
||||
className={styles.iconBtn}
|
||||
aria-label="Customer dashboard"
|
||||
title="Customer dashboard"
|
||||
>
|
||||
<img
|
||||
className={styles.userIcon}
|
||||
src="/images/icons/user.svg"
|
||||
alt=""
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
|
||||
<span className={styles.actionsDivider} aria-hidden />
|
||||
<div className={styles.lang} ref={langRef}>
|
||||
<button
|
||||
className={styles.langTrigger}
|
||||
type="button"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={langOpen}
|
||||
aria-label="Select language"
|
||||
onClick={() => setLangOpen((value) => !value)}
|
||||
>
|
||||
{language.label}
|
||||
<img
|
||||
className={`${styles.langChevron} ${langOpen ? styles.langChevronOpen : ""}`}
|
||||
src="/images/icons/chevron.svg"
|
||||
alt=""
|
||||
width={7}
|
||||
height={12}
|
||||
/>
|
||||
</button>
|
||||
{langOpen ? (
|
||||
<ul className={styles.langMenu} role="listbox" aria-label="Languages">
|
||||
{languages.map((item) => (
|
||||
<li
|
||||
key={item.code}
|
||||
role="option"
|
||||
aria-selected={item.code === language.code}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={
|
||||
item.code === language.code
|
||||
? styles.langOptionActive
|
||||
: styles.langOption
|
||||
}
|
||||
onClick={() => selectLanguage(item.code)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={styles.burger}
|
||||
type="button"
|
||||
aria-label="Open menu"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
>
|
||||
<img src="/images/icons/menu.svg" alt="" width={22} height={22} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{megaOpen ? (
|
||||
<div className={styles.megaHost} onMouseEnter={showMega}>
|
||||
<MegaMenu
|
||||
tabs={tabs}
|
||||
tab={activeTab}
|
||||
menuImages={menuImages}
|
||||
onTabHover={setActiveTabId}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{open ? (
|
||||
<nav className={styles.mobileNav} aria-label="Mobile">
|
||||
<a href="/" onClick={() => setOpen(false)}>
|
||||
Home
|
||||
</a>
|
||||
{navLinks.map((link) => (
|
||||
<a key={link.label} href={link.href} onClick={() => setOpen(false)}>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
<div className={styles.mobileMega}>
|
||||
{tabs.map((tab) => (
|
||||
<div key={tab.id}>
|
||||
<p>{tab.label}</p>
|
||||
{tab.groups.map((group) => (
|
||||
<div key={`${tab.id}-${group.title}`}>
|
||||
<p>{group.title}</p>
|
||||
{group.links.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
key={`${tab.id}-${group.title}-${item.label}-${item.href}`}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.mobileLang}>
|
||||
<span>Language</span>
|
||||
<div className={styles.mobileLangOptions}>
|
||||
{languages.map((item) => (
|
||||
<button
|
||||
key={item.code}
|
||||
type="button"
|
||||
className={
|
||||
item.code === language.code
|
||||
? styles.langOptionActive
|
||||
: styles.langOption
|
||||
}
|
||||
onClick={() => {
|
||||
selectLanguage(item.code);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.mobileAccount}>
|
||||
{user ? (
|
||||
<>
|
||||
<p className={styles.mobileAccountName}>{firstName}</p>
|
||||
<a href={customerUrl} onClick={() => setOpen(false)}>
|
||||
Customer dashboard
|
||||
</a>
|
||||
<a
|
||||
href={`${customerUrl}/profile`}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
My profile
|
||||
</a>
|
||||
<a
|
||||
href={`${customerUrl}/forgot-password`}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Change password
|
||||
</a>
|
||||
{ownsBusiness ? (
|
||||
<a href={businessUrl} onClick={() => setOpen(false)}>
|
||||
Business dashboard
|
||||
</a>
|
||||
) : null}
|
||||
{isSuperAdmin ? (
|
||||
<a href={adminUrl} onClick={() => setOpen(false)}>
|
||||
Super admin
|
||||
</a>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
logout();
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<a
|
||||
href={getCustomerLoginUrl("/")}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.mobileActions}>
|
||||
<a href="/register" className={styles.iconBtn} aria-label="Join">
|
||||
<img src="/images/icons/add.svg" alt="" width={30} height={30} />
|
||||
</a>
|
||||
<button className={styles.iconBtn} type="button" aria-label="Search">
|
||||
<img src="/images/icons/search.svg" alt="" width={30} height={30} />
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
) : null}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
.panel {
|
||||
position: absolute;
|
||||
top: calc(100% - 1px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: min(var(--container-width), calc(100vw - 2 * var(--page-pad)));
|
||||
background: var(--color-white);
|
||||
box-shadow: 0 22px 48px rgba(0, 0, 0, 0.1);
|
||||
z-index: 60;
|
||||
}
|
||||
|
||||
.inner {
|
||||
display: grid;
|
||||
grid-template-columns: 210px minmax(0, 1fr);
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 22px 0;
|
||||
border-right: 1px solid var(--color-card);
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: grid;
|
||||
grid-template-columns: 18px 1fr 10px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 13px 18px 13px 22px;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1.6px;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-body);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tab img {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
object-fit: contain;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.tabActive {
|
||||
background: var(--color-card);
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.tabActive img {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tabChevron {
|
||||
width: 7px;
|
||||
height: 12px;
|
||||
justify-self: end;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tabChevronSlot {
|
||||
width: 7px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(240px, 0.8fr);
|
||||
gap: 8px;
|
||||
padding: 32px 24px 28px 40px;
|
||||
}
|
||||
|
||||
.groups {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 28px 40px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.group h3 {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1.8px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.group a {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1.5px;
|
||||
text-transform: uppercase;
|
||||
padding: 4px 0;
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.group a:hover {
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.featured {
|
||||
position: relative;
|
||||
min-height: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.featuredTitle {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 170px;
|
||||
font-size: 26px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 3.5px;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.featuredTitle span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.featuredImage {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 94%;
|
||||
width: auto;
|
||||
max-width: 72%;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dress {
|
||||
right: -2%;
|
||||
height: 108%;
|
||||
}
|
||||
|
||||
.drum,
|
||||
.notebook,
|
||||
.shoes {
|
||||
right: 4%;
|
||||
height: 82%;
|
||||
bottom: 8%;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.panel {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import type { MegaTab } from "@/data/home";
|
||||
import styles from "./MegaMenu.module.css";
|
||||
|
||||
const imageClassMap: Record<string, string> = {
|
||||
dress: styles.dress,
|
||||
drum: styles.drum,
|
||||
notebook: styles.notebook,
|
||||
shoes: styles.shoes,
|
||||
};
|
||||
|
||||
export default function MegaMenu({
|
||||
tabs,
|
||||
tab,
|
||||
menuImages = [],
|
||||
onTabHover,
|
||||
}: {
|
||||
tabs: MegaTab[];
|
||||
tab: MegaTab;
|
||||
menuImages?: string[];
|
||||
onTabHover: (id: string) => void;
|
||||
}) {
|
||||
const tabIndex = Math.max(
|
||||
0,
|
||||
tabs.findIndex((item) => item.id === tab.id),
|
||||
);
|
||||
const uploaded =
|
||||
menuImages[tabIndex]?.trim() ||
|
||||
(tabIndex !== 3 ? menuImages[3]?.trim() : "") ||
|
||||
"";
|
||||
const imageSrc = uploaded || tab.featured.image;
|
||||
const imageClass =
|
||||
!uploaded && tab.featured.imageClass
|
||||
? imageClassMap[tab.featured.imageClass]
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className={styles.panel} id="categories-mega">
|
||||
<div className={styles.inner}>
|
||||
<div className={styles.tabs} role="tablist" aria-label="Category types">
|
||||
{tabs.map((item) => {
|
||||
const active = item.id === tab.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
id={`mega-tab-${item.id}`}
|
||||
aria-selected={active}
|
||||
aria-controls="mega-tab-panel"
|
||||
className={`${styles.tab} ${active ? styles.tabActive : ""}`}
|
||||
onMouseEnter={() => onTabHover(item.id)}
|
||||
onFocus={() => onTabHover(item.id)}
|
||||
onClick={() => onTabHover(item.id)}
|
||||
>
|
||||
<img src={item.icon} alt="" width={15} height={15} />
|
||||
<span>{item.label}</span>
|
||||
{active ? (
|
||||
<img
|
||||
className={styles.tabChevron}
|
||||
src="/images/icons/chevron.svg"
|
||||
alt=""
|
||||
width={7}
|
||||
height={12}
|
||||
/>
|
||||
) : (
|
||||
<span className={styles.tabChevronSlot} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.body}
|
||||
role="tabpanel"
|
||||
id="mega-tab-panel"
|
||||
aria-labelledby={`mega-tab-${tab.id}`}
|
||||
>
|
||||
<div className={styles.groups}>
|
||||
{tab.groups.map((group, index) => (
|
||||
<div className={styles.group} key={`${group.title}-${index}`}>
|
||||
<h3>{group.title}</h3>
|
||||
{group.links.map((link, linkIndex) => (
|
||||
<a href={link.href} key={`${link.label}-${linkIndex}`}>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.featured}>
|
||||
<img
|
||||
className={`${styles.featuredImage} ${imageClass ?? ""}`}
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
.gallery {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.main {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid #e4e4e4;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
padding: 4.2%;
|
||||
}
|
||||
|
||||
.mainInner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mainImg {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.thumbs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 144px));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.thumb,
|
||||
.thumbActive {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid #e4e4e4;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.thumbActive {
|
||||
border-color: #111;
|
||||
}
|
||||
|
||||
.thumbInner {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.thumbImg {
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.thumb:hover .thumbImg {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import styles from "./PortfolioGallery.module.css";
|
||||
|
||||
export default function PortfolioGallery({ images }: { images: string[] }) {
|
||||
const [active, setActive] = useState(0);
|
||||
const thumbs = images.slice(0, 3);
|
||||
|
||||
return (
|
||||
<div className={styles.gallery}>
|
||||
<div className={styles.main}>
|
||||
<div className={styles.mainInner}>
|
||||
<Image
|
||||
src={images[active] ?? images[0]}
|
||||
alt=""
|
||||
fill
|
||||
className={styles.mainImg}
|
||||
sizes="(max-width: 900px) 100vw, 830px"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.thumbs}>
|
||||
{thumbs.map((src, index) => (
|
||||
<button
|
||||
key={`${src}-${index}`}
|
||||
type="button"
|
||||
className={active === index ? styles.thumbActive : styles.thumb}
|
||||
onClick={() => setActive(index)}
|
||||
aria-label={`Show image ${index + 1}`}
|
||||
>
|
||||
<span className={styles.thumbInner}>
|
||||
<Image
|
||||
src={src}
|
||||
alt=""
|
||||
fill
|
||||
className={styles.thumbImg}
|
||||
sizes="144px"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
height: fit-content;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.coverWrap {
|
||||
position: relative;
|
||||
height: 150px;
|
||||
overflow: hidden;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.cover {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.coverWrap > img.cover {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border: 1px solid var(--color-card);
|
||||
border-top: 0;
|
||||
border-radius: 0 0 5px 5px;
|
||||
padding: 0 16px 14px;
|
||||
background: var(--color-white);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Hovering the cover (or anywhere on the card) also runs the body wipe. */
|
||||
.link:hover .body:global(.tile-hover)::before,
|
||||
.link:focus-within .body:global(.tile-hover)::before {
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
|
||||
.link:hover .body:global(.tile-hover) :is(h1, h2, h3, p, span, strong, a),
|
||||
.link:focus-within .body:global(.tile-hover) :is(h1, h2, h3, p, span, strong, a) {
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.info {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: -36px;
|
||||
background: var(--color-card);
|
||||
border-radius: 5px;
|
||||
padding: 12px 14px 10px;
|
||||
}
|
||||
|
||||
.info > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Keep the gray identity panel readable while the body wipe fills below. */
|
||||
.link:hover .body:global(.tile-hover) .info .name,
|
||||
.link:focus-within .body:global(.tile-hover) .info .name {
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.link:hover .body:global(.tile-hover) .info .tagline,
|
||||
.link:focus-within .body:global(.tile-hover) .info .tagline {
|
||||
color: var(--color-muted);
|
||||
}
|
||||
|
||||
.link:hover .body:global(.tile-hover) .info .meta,
|
||||
.link:focus-within .body:global(.tile-hover) .info .meta,
|
||||
.link:hover .body:global(.tile-hover) .info .products,
|
||||
.link:focus-within .body:global(.tile-hover) .info .products,
|
||||
.link:hover .body:global(.tile-hover) .info .since,
|
||||
.link:focus-within .body:global(.tile-hover) .info .since,
|
||||
.link:hover .body:global(.tile-hover) .info .products strong,
|
||||
.link:focus-within .body:global(.tile-hover) .info .products strong,
|
||||
.link:hover .body:global(.tile-hover) .info .since strong,
|
||||
.link:focus-within .body:global(.tile-hover) .info .since strong {
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
.link:hover .body:global(.tile-hover) .info .divider,
|
||||
.link:focus-within .body:global(.tile-hover) .info .divider {
|
||||
background: #cfcfcf;
|
||||
}
|
||||
|
||||
.identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 36px;
|
||||
height: 30px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo.wide {
|
||||
width: 62px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.logo.round {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
object-fit: cover;
|
||||
margin-top: -12px;
|
||||
}
|
||||
|
||||
.logo.square {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
object-fit: cover;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 2.4px;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 11px;
|
||||
font-weight: 300;
|
||||
color: var(--color-muted);
|
||||
margin-top: 2px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 6px;
|
||||
color: var(--color-dark);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.products strong,
|
||||
.since strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.products {
|
||||
font-size: 10px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.products strong {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.since {
|
||||
font-size: 10px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.since strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: #cfcfcf;
|
||||
}
|
||||
|
||||
.description {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
line-height: 18px;
|
||||
color: var(--color-muted);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.rule {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 1px;
|
||||
background: var(--color-card);
|
||||
margin: 10px 0 8px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.link:hover .body:global(.tile-hover) .rule,
|
||||
.link:focus-within .body:global(.tile-hover) .rule {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.footerRow {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 300;
|
||||
color: var(--color-soft);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.rowArrow {
|
||||
width: 8px;
|
||||
height: 14px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.55;
|
||||
transition: opacity 0.3s ease, filter 0.3s ease;
|
||||
}
|
||||
|
||||
.link:hover .body:global(.tile-hover) .rowArrow,
|
||||
.link:focus-within .body:global(.tile-hover) .rowArrow {
|
||||
opacity: 1;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.rule,
|
||||
.tag,
|
||||
.rowArrow,
|
||||
.description {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import type { Producer } from "@/data/home";
|
||||
import styles from "./ProducerCard.module.css";
|
||||
|
||||
function isRemoteSrc(src: string) {
|
||||
return src.startsWith("http://") || src.startsWith("https://");
|
||||
}
|
||||
|
||||
export default function ProducerCard({ producer }: { producer: Producer }) {
|
||||
return (
|
||||
<article className={styles.card}>
|
||||
<Link href={`/businesses/${producer.slug}`} className={styles.link}>
|
||||
<div className={styles.coverWrap}>
|
||||
{isRemoteSrc(producer.cover) ? (
|
||||
<img
|
||||
src={producer.cover}
|
||||
alt=""
|
||||
className={styles.cover}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={producer.cover}
|
||||
alt=""
|
||||
fill
|
||||
className={styles.cover}
|
||||
sizes="(max-width: 720px) 100vw, (max-width: 1100px) 50vw, 430px"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={`tile-hover ${styles.body}`}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.identity}>
|
||||
<img
|
||||
className={`${styles.logo} ${producer.logoClass ? styles[producer.logoClass] : ""}`}
|
||||
src={producer.logo}
|
||||
alt=""
|
||||
width={40}
|
||||
height={33}
|
||||
/>
|
||||
<div>
|
||||
<h3 className={styles.name}>{producer.name}</h3>
|
||||
{producer.tagline ? (
|
||||
<p className={styles.tagline}>{producer.tagline}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.meta}>
|
||||
<p className={styles.products}>
|
||||
<strong>{producer.products} </strong>
|
||||
Products
|
||||
</p>
|
||||
<span className={styles.divider} />
|
||||
<p className={styles.since}>
|
||||
Since <strong>{producer.since}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{producer.description ? (
|
||||
<p className={styles.description}>{producer.description}</p>
|
||||
) : null}
|
||||
<div className={styles.rule} />
|
||||
<div className={styles.footerRow}>
|
||||
<span className={styles.tag}>{producer.category}</span>
|
||||
<img
|
||||
className={styles.rowArrow}
|
||||
src="/images/icons/chevron.svg"
|
||||
alt=""
|
||||
width={8}
|
||||
height={14}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import {
|
||||
clearTokens,
|
||||
getAccessToken,
|
||||
getCustomerLoginUrl,
|
||||
} from "@/lib/session";
|
||||
|
||||
const MESHKEE_API_URL =
|
||||
process.env.NEXT_PUBLIC_MESHKEE_API_URL?.replace(/\/$/, "") ||
|
||||
"https://api.meshkee.com/api/v1";
|
||||
|
||||
async function verifySession(accessToken: string, signal: AbortSignal) {
|
||||
const response = await fetch(`${MESHKEE_API_URL}/auth/me`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
signal,
|
||||
cache: "no-store",
|
||||
});
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
export default function RequireAuth({
|
||||
children,
|
||||
returnPath = "/register",
|
||||
}: {
|
||||
children: ReactNode;
|
||||
returnPath?: string;
|
||||
}) {
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
async function ensureAuth() {
|
||||
const token = getAccessToken();
|
||||
if (!token) {
|
||||
window.location.replace(getCustomerLoginUrl(returnPath));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const ok = await verifySession(token, controller.signal);
|
||||
if (controller.signal.aborted) return;
|
||||
if (!ok) {
|
||||
clearTokens();
|
||||
window.location.replace(getCustomerLoginUrl(returnPath));
|
||||
return;
|
||||
}
|
||||
setReady(true);
|
||||
} catch {
|
||||
if (controller.signal.aborted) return;
|
||||
clearTokens();
|
||||
window.location.replace(getCustomerLoginUrl(returnPath));
|
||||
}
|
||||
}
|
||||
|
||||
void ensureAuth();
|
||||
return () => controller.abort();
|
||||
}, [returnPath]);
|
||||
|
||||
if (!ready) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: "50vh",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
fontSize: 12,
|
||||
letterSpacing: "0.2em",
|
||||
textTransform: "uppercase",
|
||||
color: "#8e8e8e",
|
||||
}}
|
||||
>
|
||||
Checking account…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.action {
|
||||
min-width: 135px;
|
||||
min-height: 35px;
|
||||
padding: 0 18px;
|
||||
font-size: 16px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.action img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
object-fit: contain;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
:global(.filter-chip).action:hover img,
|
||||
:global(.filter-chip).action:focus-visible img {
|
||||
filter: invert(1);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import styles from "./ShareLikeActions.module.css";
|
||||
|
||||
export default function ShareLikeActions() {
|
||||
return (
|
||||
<div className={styles.actions}>
|
||||
<button className={`filter-chip ${styles.action}`} type="button">
|
||||
<img src="/images/icons/share.svg" alt="" width={18} height={18} />
|
||||
share
|
||||
</button>
|
||||
<button className={`filter-chip ${styles.action}`} type="button">
|
||||
<img src="/images/icons/heart.svg" alt="" width={18} height={18} />
|
||||
LIKE
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import { featuredProducers } from "@/data/home";
|
||||
import { defaultBlogPosts, defaultPortfolios } from "@/data/details";
|
||||
|
||||
export type BusinessTabId =
|
||||
| "products"
|
||||
| "contact"
|
||||
| "blog"
|
||||
| "gallery"
|
||||
| "portfolios";
|
||||
|
||||
export type BusinessDetail = {
|
||||
slug: string;
|
||||
name: string;
|
||||
tagline: string;
|
||||
breadcrumbs: string[];
|
||||
hero: string;
|
||||
logo: string;
|
||||
since: string;
|
||||
province: string;
|
||||
activeProducts: string;
|
||||
telephone: string;
|
||||
description: string;
|
||||
productCategories: string[];
|
||||
products: {
|
||||
id: string;
|
||||
title: string;
|
||||
image: string;
|
||||
rating: string;
|
||||
minOrder: string;
|
||||
shipsIn: string;
|
||||
category: string;
|
||||
}[];
|
||||
contact: {
|
||||
address: string;
|
||||
landlines: string[];
|
||||
socials: { network: string; handle: string; icon: string }[];
|
||||
map: string;
|
||||
};
|
||||
blogCategories: string[];
|
||||
posts: {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
image: string;
|
||||
date: string;
|
||||
author: string;
|
||||
}[];
|
||||
gallery: { id: string; image: string; tall?: boolean; medium?: boolean }[];
|
||||
portfolioCategories: string[];
|
||||
portfolios: {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const businessTabs: { id: BusinessTabId; label: string }[] = [
|
||||
{ id: "products", label: "Products" },
|
||||
{ id: "contact", label: "Contact info" },
|
||||
{ id: "blog", label: "blog" },
|
||||
{ id: "gallery", label: "gallery" },
|
||||
{ id: "portfolios", label: "Portfolios" },
|
||||
];
|
||||
|
||||
const productImage = "/images/business/product-necklace.png";
|
||||
const productAlt = "/images/business/product-alt.png";
|
||||
const productRing = "/images/business/product-ring.png";
|
||||
const galleryImage = "/images/business/gallery.jpg";
|
||||
const blogImage = "/images/business/blog.jpg";
|
||||
|
||||
export const businesses: BusinessDetail[] = [
|
||||
{
|
||||
slug: "ayoubi-gallery",
|
||||
name: "ayoubi gallery",
|
||||
tagline: "we produce bread materials",
|
||||
breadcrumbs: ["producers", "industry", "Food", "processed"],
|
||||
hero: "/images/business/hero.jpg",
|
||||
logo: "/images/business/logo.png",
|
||||
since: "2018",
|
||||
province: "Tehran",
|
||||
activeProducts: "121",
|
||||
telephone: "+9821-31634",
|
||||
description:
|
||||
"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. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
productCategories: ["All", "Neckleses", "Earrings", "Bracelet"],
|
||||
products: Array.from({ length: 6 }, (_, i) => ({
|
||||
id: `p-${i + 1}`,
|
||||
title: "Gold scarlet necklace",
|
||||
image: [productImage, productAlt, productRing][i % 3],
|
||||
rating: "4.3",
|
||||
minOrder: "3",
|
||||
shipsIn: "7",
|
||||
category: ["Neckleses", "Earrings", "Bracelet"][i % 3],
|
||||
})),
|
||||
contact: {
|
||||
address: "Tehran, Jordan St., beside Sina Bank, number 27",
|
||||
landlines: ["+98 212243248", "+98 212243249"],
|
||||
socials: [
|
||||
{
|
||||
network: "instagram",
|
||||
handle: "ayoubi.jewelery",
|
||||
icon: "/images/icons/instagram.png",
|
||||
},
|
||||
{
|
||||
network: "Telegram",
|
||||
handle: "ayoubi.jewelery",
|
||||
icon: "/images/icons/telegram.png",
|
||||
},
|
||||
{
|
||||
network: "Facebook",
|
||||
handle: "ayoubi.jewelery",
|
||||
icon: "/images/icons/facebook.png",
|
||||
},
|
||||
{
|
||||
network: "Whatsapp",
|
||||
handle: "ayoubi.jewelery",
|
||||
icon: "/images/icons/telegram.png",
|
||||
},
|
||||
],
|
||||
map: "/images/business/map.png",
|
||||
},
|
||||
blogCategories: ["All", "Neckleses", "Earrings", "Bracelet"],
|
||||
posts: Array.from({ length: 6 }, (_, i) => ({
|
||||
id: `b-${i + 1}`,
|
||||
slug: i === 0 ? "gold-scarlet-necklace" : `gold-scarlet-necklace-${i + 1}`,
|
||||
title: "Gold scarlet necklace",
|
||||
excerpt:
|
||||
"we will show your business to verity of visitor around the world.we will show your business to verity of visitor around the world.we will show your business to verity of visitor around the world.",
|
||||
image: "/images/blog/thumb.jpg",
|
||||
date: "September 11",
|
||||
author: "Mohammadi",
|
||||
})),
|
||||
gallery: [
|
||||
{ id: "g1", image: galleryImage, tall: true },
|
||||
{ id: "g2", image: productAlt },
|
||||
{ id: "g3", image: blogImage, medium: true },
|
||||
{ id: "g4", image: productImage },
|
||||
{ id: "g5", image: "/images/producers/cover-jewelry.png", tall: true },
|
||||
{ id: "g6", image: productRing, tall: true },
|
||||
{ id: "g7", image: galleryImage },
|
||||
],
|
||||
portfolioCategories: ["All", "Ring", "Earrings", "Bracelet"],
|
||||
portfolios: [
|
||||
{
|
||||
id: "pf1",
|
||||
slug: "gold-scarlet-necklace",
|
||||
title: "Gold scarlet necklace",
|
||||
description: "we will show your business to verity of visitor ",
|
||||
image: "/images/portfolio/main.jpg",
|
||||
},
|
||||
{
|
||||
id: "pf2",
|
||||
slug: "gold-scarlet-necklace-2",
|
||||
title: "Gold scarlet necklace",
|
||||
description: "we will show your business to verity of visitor ",
|
||||
image: "/images/business/portfolio-1.jpg",
|
||||
},
|
||||
{
|
||||
id: "pf3",
|
||||
slug: "gold-scarlet-necklace-3",
|
||||
title: "Gold scarlet necklace",
|
||||
description: "we will show your business to verity of visitor ",
|
||||
image: "/images/business/portfolio-3.jpg",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function fromProducer(slug: string): BusinessDetail | undefined {
|
||||
const producer = featuredProducers.find((p) => p.slug === slug);
|
||||
if (!producer) return undefined;
|
||||
const base = businesses[0];
|
||||
return {
|
||||
...base,
|
||||
slug: producer.slug,
|
||||
name: producer.name,
|
||||
tagline: producer.tagline,
|
||||
logo: producer.logo,
|
||||
activeProducts: producer.products,
|
||||
since: producer.since,
|
||||
};
|
||||
}
|
||||
|
||||
export function getBusinessBySlug(slug: string) {
|
||||
return businesses.find((b) => b.slug === slug) ?? fromProducer(slug);
|
||||
}
|
||||
|
||||
export function getAllBusinessSlugs() {
|
||||
const detailSlugs = businesses.map((b) => b.slug);
|
||||
const producerSlugs = featuredProducers.map((p) => p.slug);
|
||||
return [...new Set([...detailSlugs, ...producerSlugs])];
|
||||
}
|
||||
|
||||
export function getBusinessBlogPost(businessSlug: string, postSlug: string) {
|
||||
const business = getBusinessBySlug(businessSlug);
|
||||
if (!business) return null;
|
||||
const list = defaultBlogPosts.map((post, i) => ({
|
||||
...post,
|
||||
image: business.posts[i]?.image ?? post.image,
|
||||
slug: business.posts[i]?.slug ?? post.slug,
|
||||
}));
|
||||
const post = list.find((p) => p.slug === postSlug);
|
||||
if (!post) return null;
|
||||
const related = list.filter((p) => p.slug !== post.slug).slice(0, 4);
|
||||
return { business, post: { ...post, hero: "/images/blog/hero.jpg" }, related };
|
||||
}
|
||||
|
||||
export function getBusinessPortfolio(businessSlug: string, portfolioSlug: string) {
|
||||
const business = getBusinessBySlug(businessSlug);
|
||||
if (!business) return null;
|
||||
const list = defaultPortfolios.map((item, i) => ({
|
||||
...item,
|
||||
slug: business.portfolios[i]?.slug ?? item.slug,
|
||||
image: business.portfolios[i]?.image ?? item.image,
|
||||
images:
|
||||
i === 0
|
||||
? item.images
|
||||
: [
|
||||
business.portfolios[i]?.image ?? item.image,
|
||||
...item.images.slice(1),
|
||||
],
|
||||
}));
|
||||
const portfolio = list.find((p) => p.slug === portfolioSlug);
|
||||
if (!portfolio) return null;
|
||||
return { business, portfolio };
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
export type BlogPost = {
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
image: string;
|
||||
date: string;
|
||||
author: string;
|
||||
category: string;
|
||||
intro: string;
|
||||
body: { type: "p" | "quote" | "rich"; text: string; linkText?: string; linkHref?: string; afterLink?: string }[];
|
||||
};
|
||||
|
||||
export type PortfolioItem = {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
images: string[];
|
||||
date: string;
|
||||
author: string;
|
||||
category: string;
|
||||
body: { type: "p" | "quote" | "rich"; text: string; linkText?: string; linkHref?: string; afterLink?: string }[];
|
||||
};
|
||||
|
||||
export const blogDetailBody: BlogPost["body"] = [
|
||||
{
|
||||
type: "p",
|
||||
text: "Lorem ipsum, or lipsum as it is sometimes known, is dummy text used in laying out print, graphic or web designs. The passage is attributed to an unknown typesetter in the 15th century who is thought to have scrambled parts of Cicero's De Finibus Bonorum et Malorum for use in a type specimen book. It usually begins with:",
|
||||
},
|
||||
{
|
||||
type: "quote",
|
||||
text: "“Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.”",
|
||||
},
|
||||
{
|
||||
type: "rich",
|
||||
text: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn't distract from the layout. A practice not without ",
|
||||
linkText: "controversy",
|
||||
linkHref: "https://loremipsum.io/#controversy",
|
||||
afterLink:
|
||||
", laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.",
|
||||
},
|
||||
{
|
||||
type: "rich",
|
||||
text: "The passage experienced a surge in popularity during the 1960s when Letraset used it on their dry-transfer sheets, and again during the 90s as desktop publishers bundled the text with their software. Today it's seen all around the web; on templates, websites, and stock designs. Use our ",
|
||||
linkText: "generator",
|
||||
linkHref: "https://loremipsum.io/#generator",
|
||||
afterLink: " to get your own, or read on for the authoritative history of lorem ipsum.",
|
||||
},
|
||||
];
|
||||
|
||||
export const portfolioDetailBody: PortfolioItem["body"] = blogDetailBody;
|
||||
|
||||
export const defaultBlogPosts: BlogPost[] = Array.from({ length: 6 }, (_, i) => ({
|
||||
slug: i === 0 ? "gold-scarlet-necklace" : `gold-scarlet-necklace-${i + 1}`,
|
||||
title: "Gold scarlet necklace",
|
||||
excerpt:
|
||||
"we will show your business to verity of visitor around the world.we will show your business to verity of visitor around the world.we will show your business to verity of visitor around the world.",
|
||||
image: "/images/blog/thumb.jpg",
|
||||
date: "September 11",
|
||||
author: "Mohammadi",
|
||||
category: "Bracelet",
|
||||
intro:
|
||||
"we will show your business to verity of visitor around the world.we will show your business to verity of visitor around the world.we will show your business to verity of visitor around the world.",
|
||||
body: blogDetailBody,
|
||||
}));
|
||||
|
||||
export const defaultPortfolios: PortfolioItem[] = [
|
||||
{
|
||||
slug: "gold-scarlet-necklace",
|
||||
title: "Gold scarlet necklace",
|
||||
description: "we will show your business to verity of visitor around the world.we",
|
||||
image: "/images/portfolio/main.jpg",
|
||||
images: [
|
||||
"/images/portfolio/main.jpg",
|
||||
"/images/portfolio/thumb-1.jpg",
|
||||
"/images/portfolio/thumb-2.jpg",
|
||||
"/images/portfolio/thumb-3.jpg",
|
||||
],
|
||||
date: "September 11",
|
||||
author: "Mohammadi",
|
||||
category: "Bracelet",
|
||||
body: portfolioDetailBody,
|
||||
},
|
||||
{
|
||||
slug: "gold-scarlet-necklace-2",
|
||||
title: "Gold scarlet necklace",
|
||||
description: "we will show your business to verity of visitor ",
|
||||
image: "/images/business/portfolio-1.jpg",
|
||||
images: [
|
||||
"/images/business/portfolio-1.jpg",
|
||||
"/images/portfolio/thumb-1.jpg",
|
||||
"/images/portfolio/thumb-2.jpg",
|
||||
"/images/portfolio/thumb-3.jpg",
|
||||
],
|
||||
date: "September 11",
|
||||
author: "Mohammadi",
|
||||
category: "Bracelet",
|
||||
body: portfolioDetailBody,
|
||||
},
|
||||
{
|
||||
slug: "gold-scarlet-necklace-3",
|
||||
title: "Gold scarlet necklace",
|
||||
description: "we will show your business to verity of visitor ",
|
||||
image: "/images/business/portfolio-3.jpg",
|
||||
images: [
|
||||
"/images/business/portfolio-3.jpg",
|
||||
"/images/portfolio/thumb-1.jpg",
|
||||
"/images/portfolio/thumb-2.jpg",
|
||||
"/images/portfolio/thumb-3.jpg",
|
||||
],
|
||||
date: "September 11",
|
||||
author: "Mohammadi",
|
||||
category: "Bracelet",
|
||||
body: portfolioDetailBody,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,292 @@
|
||||
export const navLinks = [
|
||||
{ href: "/businesses", label: "Categories", hasMenu: true },
|
||||
{ href: "/#vision", label: "vision" },
|
||||
{ href: "/about", label: "about us" },
|
||||
{ href: "/contact", label: "contact us" },
|
||||
] as const;
|
||||
|
||||
export type MegaLink = { label: string; href: string };
|
||||
|
||||
export type MegaGroup = {
|
||||
title: string;
|
||||
links: MegaLink[];
|
||||
};
|
||||
|
||||
export type MegaTab = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
groups: MegaGroup[];
|
||||
featured: {
|
||||
title: string;
|
||||
image: string;
|
||||
imageClass?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const placeholders = (count: number): MegaLink[] =>
|
||||
Array.from({ length: count }, () => ({
|
||||
label: "Subcategory 1",
|
||||
href: "/businesses",
|
||||
}));
|
||||
|
||||
export const megaMenuTabs: MegaTab[] = [
|
||||
{
|
||||
id: "industry",
|
||||
label: "Industry",
|
||||
icon: "/images/icons/gear.svg",
|
||||
groups: [
|
||||
{
|
||||
title: "Materials",
|
||||
links: [
|
||||
{ label: "Metals", href: "/businesses" },
|
||||
{ label: "Plastics", href: "/businesses" },
|
||||
{ label: "Chemicals", href: "/businesses" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Category",
|
||||
links: placeholders(5),
|
||||
},
|
||||
{
|
||||
title: "Category",
|
||||
links: placeholders(6),
|
||||
},
|
||||
{
|
||||
title: "Category",
|
||||
links: placeholders(5),
|
||||
},
|
||||
],
|
||||
featured: {
|
||||
title: "Iranian industry",
|
||||
image: "/images/categories/industry.png",
|
||||
imageClass: "drum",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "stores",
|
||||
label: "Stores",
|
||||
icon: "/images/icons/cart.svg",
|
||||
groups: [
|
||||
{
|
||||
title: "Wearables",
|
||||
links: [
|
||||
{ label: "Clothes", href: "/businesses" },
|
||||
{ label: "Shoes", href: "/businesses" },
|
||||
{ label: "Accessories", href: "/businesses" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Category",
|
||||
links: placeholders(5),
|
||||
},
|
||||
{
|
||||
title: "Category",
|
||||
links: placeholders(6),
|
||||
},
|
||||
{
|
||||
title: "Category",
|
||||
links: placeholders(5),
|
||||
},
|
||||
],
|
||||
featured: {
|
||||
title: "Persian unique style",
|
||||
image: "/images/categories/clothes.png",
|
||||
imageClass: "dress",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "wholesellers",
|
||||
label: "Whole sellers",
|
||||
icon: "/images/icons/gear.svg",
|
||||
groups: [
|
||||
{
|
||||
title: "Apparel",
|
||||
links: [
|
||||
{ label: "Clothes", href: "/businesses" },
|
||||
{ label: "Shoes", href: "/businesses" },
|
||||
{ label: "Textiles", href: "/businesses" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Category",
|
||||
links: placeholders(5),
|
||||
},
|
||||
{
|
||||
title: "Category",
|
||||
links: placeholders(6),
|
||||
},
|
||||
{
|
||||
title: "Category",
|
||||
links: placeholders(5),
|
||||
},
|
||||
],
|
||||
featured: {
|
||||
title: "Wholesale market",
|
||||
image: "/images/categories/shoes.png",
|
||||
imageClass: "shoes",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "services",
|
||||
label: "Services",
|
||||
icon: "/images/icons/home-outline.svg",
|
||||
groups: [
|
||||
{
|
||||
title: "Business",
|
||||
links: [
|
||||
{ label: "Logistics", href: "#services" },
|
||||
{ label: "Consulting", href: "#services" },
|
||||
{ label: "Support", href: "#services" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Category",
|
||||
links: placeholders(5),
|
||||
},
|
||||
{
|
||||
title: "Category",
|
||||
links: placeholders(6),
|
||||
},
|
||||
{
|
||||
title: "Category",
|
||||
links: placeholders(5),
|
||||
},
|
||||
],
|
||||
featured: {
|
||||
title: "Professional services",
|
||||
image: "/images/categories/services.png",
|
||||
imageClass: "notebook",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const featuredCategories = [
|
||||
{
|
||||
title: "industry",
|
||||
description: "Some description about the title",
|
||||
image: "/images/categories/industry.png",
|
||||
imageClass: "industry",
|
||||
},
|
||||
{
|
||||
title: "Food",
|
||||
description: "Some description about the title",
|
||||
image: "/images/categories/food.png",
|
||||
imageClass: "food",
|
||||
},
|
||||
{
|
||||
title: "Fruits",
|
||||
description: "Some description about the title",
|
||||
image: "/images/categories/fruits.png",
|
||||
imageClass: "fruits",
|
||||
},
|
||||
{
|
||||
title: "Clothes",
|
||||
description: "Some description about the title",
|
||||
image: "/images/categories/clothes.png",
|
||||
imageClass: "clothes",
|
||||
},
|
||||
{
|
||||
title: "Home Furniture",
|
||||
description: "Some description about the title",
|
||||
image: "/images/categories/chair.png",
|
||||
imageClass: "chair",
|
||||
},
|
||||
{
|
||||
title: "Shoes",
|
||||
description: "Some description about the title",
|
||||
image: "/images/categories/shoes.png",
|
||||
imageClass: "shoes",
|
||||
},
|
||||
{
|
||||
title: "Home appliances",
|
||||
description: "Some description about the title",
|
||||
image: "/images/categories/washer.png",
|
||||
imageClass: "washer",
|
||||
},
|
||||
{
|
||||
title: "Services",
|
||||
description: "Some description about the title",
|
||||
image: "/images/categories/services.png",
|
||||
imageClass: "services",
|
||||
},
|
||||
];
|
||||
|
||||
export type Producer = {
|
||||
slug: string;
|
||||
name: string;
|
||||
tagline: string;
|
||||
products: string;
|
||||
since: string;
|
||||
description: string;
|
||||
typeIcon: "cart" | "gear";
|
||||
typeLabel: string;
|
||||
category: string;
|
||||
cover: string;
|
||||
logo: string;
|
||||
logoClass?: string;
|
||||
};
|
||||
|
||||
export const featuredProducers: Producer[] = [
|
||||
{
|
||||
slug: "sani-khani",
|
||||
name: "sani khani",
|
||||
tagline: "Lorem Ipsum",
|
||||
products: "921",
|
||||
since: "2021",
|
||||
description: "we will show your business to verity of visitor around the world.",
|
||||
typeIcon: "cart",
|
||||
typeLabel: "Store",
|
||||
category: "Home Appliances",
|
||||
cover: "/images/producers/cover-laundry.png",
|
||||
logo: "/images/producers/logo-sani.png",
|
||||
logoClass: "wide",
|
||||
},
|
||||
{
|
||||
slug: "arad-charta",
|
||||
name: "Arad charta",
|
||||
tagline: "we produce bread materials",
|
||||
products: "3",
|
||||
since: "2021",
|
||||
description: "we will show your business to verity of visitor around the world.",
|
||||
typeIcon: "gear",
|
||||
typeLabel: "Industry",
|
||||
category: "Food",
|
||||
cover: "/images/producers/cover-bakery.png",
|
||||
logo: "/images/producers/logo-arad.png",
|
||||
},
|
||||
{
|
||||
slug: "simode",
|
||||
name: "Simode",
|
||||
tagline: "Clothes whole seller",
|
||||
products: "121",
|
||||
since: "2021",
|
||||
description: "we will show your business to verity of visitor around the world.",
|
||||
typeIcon: "gear",
|
||||
typeLabel: "Whole Seller",
|
||||
category: "Clothes",
|
||||
cover: "/images/producers/cover-clothes.png",
|
||||
logo: "/images/producers/logo-simode.png",
|
||||
logoClass: "round",
|
||||
},
|
||||
{
|
||||
slug: "ayoubi-gallery",
|
||||
name: "ayoubi gallery",
|
||||
tagline: "An elegant jewellery store",
|
||||
products: "121",
|
||||
since: "2021",
|
||||
description: "we will show your business to verity of visitor around the world.",
|
||||
typeIcon: "gear",
|
||||
typeLabel: "Store",
|
||||
category: "Jewellery",
|
||||
cover: "/images/producers/cover-jewelry.png",
|
||||
logo: "/images/producers/logo-ayoubi.png",
|
||||
logoClass: "square",
|
||||
},
|
||||
];
|
||||
|
||||
export const aboutBullets = [
|
||||
"Excepteur sint occaecat cupidatat non proident",
|
||||
"veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo con",
|
||||
"Excepteur sint occaecat cupidatat non proident",
|
||||
];
|
||||
@@ -0,0 +1,97 @@
|
||||
export const aboutCopy = {
|
||||
kicker: "About Us",
|
||||
title: ["IranBay", "A Get To Iran", "Market."],
|
||||
paragraphs: [
|
||||
"n publishing and graphic design, Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content. Lorem ipsum may be used as a placeholder before final copy is available",
|
||||
"n publishing and graphic design, Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content. Lorem ipsum may be used as a placeholder before final copy is available",
|
||||
],
|
||||
};
|
||||
|
||||
export const clients = [
|
||||
{ name: "Apple", image: "/images/about/clients/apple.png" },
|
||||
{ name: "Audi", image: "/images/about/clients/audi.png" },
|
||||
{ name: "Tesla", image: "/images/about/clients/tesla.png" },
|
||||
{ name: "Mercedes", image: "/images/about/clients/mercedes.png" },
|
||||
{ name: "BMW", image: "/images/about/clients/bmw.png" },
|
||||
];
|
||||
|
||||
export const testimonials = [
|
||||
{
|
||||
body: "n publishing and graphic design, Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content. Lorem ipsum may be used as a placeholder before final copy is available",
|
||||
meta: "Alimohammadi | 21:31 | Sepember | 2023.20.16",
|
||||
},
|
||||
{
|
||||
body: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
|
||||
meta: "Sara Ahmadi | 14:05 | October | 2024.02.11",
|
||||
},
|
||||
{
|
||||
body: "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",
|
||||
meta: "Reza Karimi | 09:42 | January | 2025.01.08",
|
||||
},
|
||||
];
|
||||
|
||||
/** @deprecated use testimonials */
|
||||
export const testimonial = testimonials[0];
|
||||
|
||||
export const services = [
|
||||
{
|
||||
title: "Support",
|
||||
subtitle: "24 hour support",
|
||||
image: "/images/about/support.png",
|
||||
tone: "support",
|
||||
},
|
||||
{
|
||||
title: "Register",
|
||||
subtitle: "Quick store registration",
|
||||
image: "/images/about/register.png",
|
||||
tone: "register",
|
||||
},
|
||||
{
|
||||
title: "Responsive",
|
||||
subtitle: "Responsive in all sizes",
|
||||
image: "/images/about/responsive.png",
|
||||
tone: "responsive",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const contactInfo = {
|
||||
email: "Alimohammadi@gmail.com",
|
||||
phones: ["+98 937 622 0110", "+98 912 123 4567"],
|
||||
addressLines: [
|
||||
"Sheikh Mohammed bin Rashid Blvd",
|
||||
"Downtown Dubai - Dubai - United Arab Emirates",
|
||||
],
|
||||
};
|
||||
|
||||
export const faqs = [
|
||||
{
|
||||
question: "In publishing and graphic design?",
|
||||
answer:
|
||||
"Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content.",
|
||||
},
|
||||
{
|
||||
question: "In publishing and graphic?",
|
||||
answer:
|
||||
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
|
||||
},
|
||||
{
|
||||
question: "In publishing ?",
|
||||
answer:
|
||||
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
|
||||
},
|
||||
{
|
||||
question: "In publishing and graphic design ?",
|
||||
answer:
|
||||
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
},
|
||||
];
|
||||
|
||||
export const businessFilters = [
|
||||
{ label: "Looking for...", value: "Arad Charta" },
|
||||
{ label: "Province", value: "Tehran" },
|
||||
{ label: "City", value: "Factory" },
|
||||
];
|
||||
|
||||
export const activeFilterTags = ["Arad Charta", "Tehran"];
|
||||
|
||||
export const breadcrumbs = ["producers", "industry", "Food", "processed"];
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
clearTokens,
|
||||
getAccessToken,
|
||||
} from "@/lib/session";
|
||||
|
||||
export type WebsiteAuthUser = {
|
||||
id: string;
|
||||
cellNumber: string;
|
||||
email: string | null;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
firstNameEn: string | null;
|
||||
lastNameEn: string | null;
|
||||
roles: string[];
|
||||
dashboard: "super_admin" | "business" | "customer";
|
||||
businesses: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
isOwner: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
const MESHKEE_API_URL =
|
||||
process.env.NEXT_PUBLIC_MESHKEE_API_URL?.replace(/\/$/, "") ||
|
||||
"https://api.meshkee.com/api/v1";
|
||||
|
||||
export function getCustomerDashboardUrl() {
|
||||
return (
|
||||
process.env.NEXT_PUBLIC_CUSTOMER_DASHBOARD_URL?.replace(/\/$/, "") ||
|
||||
"http://customer.localhost:3002"
|
||||
);
|
||||
}
|
||||
|
||||
export function getAdminDashboardUrl() {
|
||||
return (
|
||||
process.env.NEXT_PUBLIC_ADMIN_DASHBOARD_URL?.replace(/\/$/, "") ||
|
||||
process.env.NEXT_PUBLIC_DASHBOARD_URL?.replace(/\/$/, "") ||
|
||||
"http://localhost:3002"
|
||||
);
|
||||
}
|
||||
|
||||
export function getBusinessDashboardUrl() {
|
||||
return (
|
||||
process.env.NEXT_PUBLIC_BUSINESS_DASHBOARD_URL?.replace(/\/$/, "") ||
|
||||
"http://business.localhost:3002"
|
||||
);
|
||||
}
|
||||
|
||||
export function displayNameFor(user: WebsiteAuthUser | null | undefined) {
|
||||
if (!user) return "";
|
||||
const first = (user.firstNameEn || user.firstName || "").trim();
|
||||
const last = (user.lastNameEn || user.lastName || "").trim();
|
||||
const full = [first, last].filter(Boolean).join(" ");
|
||||
if (full) return full;
|
||||
return user.cellNumber || "Account";
|
||||
}
|
||||
|
||||
/** Header label: first name only. */
|
||||
export function firstNameFor(user: WebsiteAuthUser | null | undefined) {
|
||||
if (!user) return "";
|
||||
const first = (user.firstNameEn || user.firstName || "").trim();
|
||||
if (first) return first.split(/\s+/)[0] || first;
|
||||
return user.cellNumber || "Account";
|
||||
}
|
||||
|
||||
export async function fetchCurrentUser(
|
||||
signal?: AbortSignal,
|
||||
): Promise<WebsiteAuthUser | null> {
|
||||
const token = getAccessToken();
|
||||
if (!token) return null;
|
||||
|
||||
const response = await fetch(`${MESHKEE_API_URL}/auth/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
signal,
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
clearTokens();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
const payload = (await response.json()) as { user?: WebsiteAuthUser };
|
||||
return payload.user ?? null;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { getAccessToken } from "@/lib/session";
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_MARKETPLACE_API_URL?.replace(/\/$/, "") ||
|
||||
"http://localhost:3001/api/v1";
|
||||
|
||||
export async function registerShop(body: {
|
||||
name: string;
|
||||
slug?: string;
|
||||
about?: string;
|
||||
meshkeeCategoryIds?: string[];
|
||||
ownerPassword: string;
|
||||
}) {
|
||||
const token = getAccessToken();
|
||||
if (!token) {
|
||||
throw new Error("Sign in required");
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/businesses/register-shop`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
typeof payload?.message === "string"
|
||||
? payload.message
|
||||
: Array.isArray(payload?.message)
|
||||
? payload.message.join(", ")
|
||||
: `Registration failed (${response.status})`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return payload as {
|
||||
listing: { id: string; slug: string; status: string };
|
||||
meshkeeBusinessId: string;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import type { MegaTab } from "@/data/home";
|
||||
import type { MeshkeeCategory } from "@/lib/meshkee-categories";
|
||||
|
||||
type TreeNode = MeshkeeCategory & { children: TreeNode[] };
|
||||
|
||||
const FEATURED_BY_SLUG: Record<
|
||||
string,
|
||||
{ title: string; image: string; imageClass?: string }
|
||||
> = {
|
||||
industry: {
|
||||
title: "Iranian industry",
|
||||
image: "/images/categories/industry.png",
|
||||
imageClass: "drum",
|
||||
},
|
||||
industries: {
|
||||
title: "Iranian industry",
|
||||
image: "/images/categories/industry.png",
|
||||
imageClass: "drum",
|
||||
},
|
||||
stores: {
|
||||
title: "Persian unique style",
|
||||
image: "/images/categories/clothes.png",
|
||||
imageClass: "dress",
|
||||
},
|
||||
wholesellers: {
|
||||
title: "Wholesale market",
|
||||
image: "/images/categories/shoes.png",
|
||||
imageClass: "shoes",
|
||||
},
|
||||
"whole-sellers": {
|
||||
title: "Wholesale market",
|
||||
image: "/images/categories/shoes.png",
|
||||
imageClass: "shoes",
|
||||
},
|
||||
services: {
|
||||
title: "Professional services",
|
||||
image: "/images/categories/services.png",
|
||||
imageClass: "notebook",
|
||||
},
|
||||
};
|
||||
|
||||
const ICON_BY_SLUG: Record<string, string> = {
|
||||
industry: "/images/icons/gear.svg",
|
||||
industries: "/images/icons/gear.svg",
|
||||
stores: "/images/icons/cart.svg",
|
||||
wholesellers: "/images/icons/gear.svg",
|
||||
"whole-sellers": "/images/icons/gear.svg",
|
||||
services: "/images/icons/home-outline.svg",
|
||||
};
|
||||
|
||||
const DEFAULT_FEATURED = {
|
||||
title: "Explore IranBay",
|
||||
image: "/images/categories/clothes.png",
|
||||
imageClass: "dress",
|
||||
};
|
||||
|
||||
function sortNodes(nodes: TreeNode[]) {
|
||||
nodes.sort(
|
||||
(a, b) =>
|
||||
a.sortOrder - b.sortOrder ||
|
||||
a.name.localeCompare(b.name, undefined, { sensitivity: "base" }),
|
||||
);
|
||||
for (const node of nodes) sortNodes(node.children);
|
||||
}
|
||||
|
||||
function buildTree(categories: MeshkeeCategory[]): TreeNode[] {
|
||||
const byId = new Map<string, TreeNode>();
|
||||
for (const category of categories) {
|
||||
byId.set(category.id, { ...category, children: [] });
|
||||
}
|
||||
|
||||
const roots: TreeNode[] = [];
|
||||
for (const node of byId.values()) {
|
||||
if (node.parentId && byId.has(node.parentId)) {
|
||||
byId.get(node.parentId)!.children.push(node);
|
||||
} else {
|
||||
roots.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
sortNodes(roots);
|
||||
return roots;
|
||||
}
|
||||
|
||||
function categoryHref(category: MeshkeeCategory) {
|
||||
const slug = category.slug?.trim();
|
||||
if (slug) return `/businesses?category=${encodeURIComponent(slug)}`;
|
||||
return `/businesses?categoryId=${encodeURIComponent(category.id)}`;
|
||||
}
|
||||
|
||||
function resolveIcon(category: MeshkeeCategory) {
|
||||
const icon = category.icon?.trim();
|
||||
if (icon) {
|
||||
if (
|
||||
icon.startsWith("/") ||
|
||||
icon.startsWith("http://") ||
|
||||
icon.startsWith("https://")
|
||||
) {
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
return ICON_BY_SLUG[category.slug] ?? "/images/icons/category.svg";
|
||||
}
|
||||
|
||||
function resolveFeatured(category: MeshkeeCategory) {
|
||||
const mapped = FEATURED_BY_SLUG[category.slug];
|
||||
if (mapped) return mapped;
|
||||
return {
|
||||
...DEFAULT_FEATURED,
|
||||
title: category.name ? `Explore ${category.name}` : DEFAULT_FEATURED.title,
|
||||
};
|
||||
}
|
||||
|
||||
/** Roots → groups (L2) → links (L3) for the website mega menu. */
|
||||
export function buildMegaMenuTabs(categories: MeshkeeCategory[]): MegaTab[] {
|
||||
const roots = buildTree(categories).filter(
|
||||
(root) => root.slug !== "other" && root.name.trim(),
|
||||
);
|
||||
|
||||
return roots.map((root) => {
|
||||
const groups = root.children.map((group) => {
|
||||
const links = group.children.length
|
||||
? group.children.map((child) => ({
|
||||
label: child.name,
|
||||
href: categoryHref(child),
|
||||
}))
|
||||
: [
|
||||
{
|
||||
label: group.name,
|
||||
href: categoryHref(group),
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
title: group.name,
|
||||
links,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: root.slug || root.id,
|
||||
label: root.name,
|
||||
icon: resolveIcon(root),
|
||||
groups:
|
||||
groups.length > 0
|
||||
? groups
|
||||
: [
|
||||
{
|
||||
title: root.name,
|
||||
links: [{ label: "Browse", href: categoryHref(root) }],
|
||||
},
|
||||
],
|
||||
featured: resolveFeatured(root),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { getAccessToken } from "@/lib/session";
|
||||
|
||||
export interface MeshkeeCategory {
|
||||
id: string;
|
||||
parentId: string | null;
|
||||
name: string;
|
||||
nameFa: string | null;
|
||||
slug: string;
|
||||
description: string | null;
|
||||
icon: string | null;
|
||||
sortOrder: number;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export function categoryLabel(category: {
|
||||
name: string;
|
||||
nameFa?: string | null;
|
||||
slug: string;
|
||||
}) {
|
||||
if (category.nameFa && category.nameFa !== category.name) {
|
||||
return `${category.name} / ${category.nameFa}`;
|
||||
}
|
||||
return category.name || category.slug;
|
||||
}
|
||||
|
||||
function mapCategory(row: Record<string, unknown>): MeshkeeCategory {
|
||||
return {
|
||||
id: String(row.id),
|
||||
parentId:
|
||||
row.parentId == null && row.parent_id == null
|
||||
? null
|
||||
: String(row.parentId ?? row.parent_id),
|
||||
name: String(row.name ?? ""),
|
||||
nameFa:
|
||||
row.nameFa == null && row.name_fa == null
|
||||
? null
|
||||
: String(row.nameFa ?? row.name_fa),
|
||||
slug: String(row.slug ?? ""),
|
||||
description: row.description == null ? null : String(row.description),
|
||||
icon: row.icon == null ? null : String(row.icon),
|
||||
sortOrder: Number(row.sortOrder ?? row.sort_order ?? 0),
|
||||
tags: Array.isArray(row.tags) ? (row.tags as string[]) : [],
|
||||
};
|
||||
}
|
||||
|
||||
export async function searchMeshkeeCategories(
|
||||
q = "",
|
||||
signal?: AbortSignal,
|
||||
): Promise<MeshkeeCategory[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (q.trim()) params.set("q", q.trim());
|
||||
|
||||
const headers = new Headers();
|
||||
const token = getAccessToken();
|
||||
if (token) headers.set("Authorization", `Bearer ${token}`);
|
||||
|
||||
const response = await fetch(`/api/meshkee/categories?${params}`, {
|
||||
headers,
|
||||
signal,
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
typeof payload?.message === "string"
|
||||
? payload.message
|
||||
: Array.isArray(payload?.message)
|
||||
? payload.message.join(", ")
|
||||
: `Failed to load categories (${response.status})`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const items = Array.isArray(payload?.items) ? payload.items : [];
|
||||
return items.map((row: Record<string, unknown>) => mapCategory(row));
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
const ACCESS_TOKEN_KEY = "marketplace_access_token";
|
||||
const REFRESH_TOKEN_KEY = "marketplace_refresh_token";
|
||||
const TOKEN_MAX_AGE_SEC = 60 * 60 * 24 * 14;
|
||||
|
||||
function cookieDomain(): string {
|
||||
const root = (process.env.NEXT_PUBLIC_ROOT_DOMAIN || "localhost").trim();
|
||||
if (!root || root === "localhost" || root.startsWith("127.")) {
|
||||
return "localhost";
|
||||
}
|
||||
return root.startsWith(".") ? root : `.${root}`;
|
||||
}
|
||||
|
||||
function readCookie(name: string): string | null {
|
||||
if (typeof document === "undefined") return null;
|
||||
const prefix = `${name}=`;
|
||||
for (const part of document.cookie.split(";")) {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed.startsWith(prefix)) {
|
||||
return decodeURIComponent(trimmed.slice(prefix.length));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function writeCookie(name: string, value: string) {
|
||||
if (typeof document === "undefined") return;
|
||||
const secure =
|
||||
typeof window !== "undefined" && window.location.protocol === "https:"
|
||||
? "; Secure"
|
||||
: "";
|
||||
document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=${TOKEN_MAX_AGE_SEC}; SameSite=Lax; Domain=${cookieDomain()}${secure}`;
|
||||
}
|
||||
|
||||
export function getAccessToken() {
|
||||
if (typeof window === "undefined") return null;
|
||||
return readCookie(ACCESS_TOKEN_KEY) || localStorage.getItem(ACCESS_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function getRefreshToken() {
|
||||
if (typeof window === "undefined") return null;
|
||||
return (
|
||||
readCookie(REFRESH_TOKEN_KEY) || localStorage.getItem(REFRESH_TOKEN_KEY)
|
||||
);
|
||||
}
|
||||
|
||||
export function setTokens(accessToken: string, refreshToken: string) {
|
||||
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
||||
writeCookie(ACCESS_TOKEN_KEY, accessToken);
|
||||
writeCookie(REFRESH_TOKEN_KEY, refreshToken);
|
||||
}
|
||||
|
||||
export function clearTokens() {
|
||||
localStorage.removeItem(ACCESS_TOKEN_KEY);
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
if (typeof document === "undefined") return;
|
||||
const domain = cookieDomain();
|
||||
document.cookie = `${ACCESS_TOKEN_KEY}=; Path=/; Max-Age=0; SameSite=Lax; Domain=${domain}`;
|
||||
document.cookie = `${REFRESH_TOKEN_KEY}=; Path=/; Max-Age=0; SameSite=Lax; Domain=${domain}`;
|
||||
}
|
||||
|
||||
export function getWebsiteOrigin() {
|
||||
return (
|
||||
process.env.NEXT_PUBLIC_WEBSITE_URL?.replace(/\/$/, "") ||
|
||||
(typeof window !== "undefined" ? window.location.origin : "http://localhost:3001")
|
||||
);
|
||||
}
|
||||
|
||||
export function getCustomerLoginUrl(returnPath = "/register") {
|
||||
const customer =
|
||||
process.env.NEXT_PUBLIC_CUSTOMER_DASHBOARD_URL?.replace(/\/$/, "") ||
|
||||
"http://customer.localhost:3002";
|
||||
const website = getWebsiteOrigin();
|
||||
const next = returnPath.startsWith("http")
|
||||
? returnPath
|
||||
: `${website}${returnPath.startsWith("/") ? returnPath : `/${returnPath}`}`;
|
||||
const url = new URL(`${customer}/login`);
|
||||
url.searchParams.set("next", next);
|
||||
return url.toString();
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { Producer } from "@/data/home";
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_MARKETPLACE_API_URL?.replace(/\/$/, "") ||
|
||||
process.env.MARKETPLACE_API_URL?.replace(/\/$/, "") ||
|
||||
"http://localhost:3001/api/v1";
|
||||
|
||||
export type WebsiteCarouselBusiness = {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
tagline: string;
|
||||
about: string;
|
||||
logoUrl: string | null;
|
||||
bannerUrl: string | null;
|
||||
categoryName: string | null;
|
||||
productCount: number;
|
||||
publishedAt: string | null;
|
||||
};
|
||||
|
||||
export type WebsiteCarousel = {
|
||||
id: string;
|
||||
title: string;
|
||||
subtext: string;
|
||||
slug: string;
|
||||
businesses: WebsiteCarouselBusiness[];
|
||||
};
|
||||
|
||||
const PLACEHOLDER_COVER = "/images/producers/cover-laundry.png";
|
||||
const PLACEHOLDER_LOGO = "/images/producers/logo-sani.png";
|
||||
const DEFAULT_DESCRIPTION =
|
||||
"we will show your business to verity of visitor around the world.";
|
||||
|
||||
export async function getWebsiteCarousels(): Promise<WebsiteCarousel[]> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/website/carousels`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok) return [];
|
||||
const data = (await response.json()) as { carousels?: WebsiteCarousel[] };
|
||||
return Array.isArray(data.carousels) ? data.carousels : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function carouselBusinessToProducer(
|
||||
business: WebsiteCarouselBusiness,
|
||||
): Producer {
|
||||
const since = business.publishedAt
|
||||
? String(new Date(business.publishedAt).getFullYear())
|
||||
: "—";
|
||||
|
||||
return {
|
||||
slug: business.slug,
|
||||
name: business.name || business.slug,
|
||||
tagline: business.tagline || "",
|
||||
products: String(business.productCount ?? 0),
|
||||
since,
|
||||
// English about when present; otherwise English placeholder.
|
||||
// Farsi about is already stripped by the API.
|
||||
description: business.about?.trim() || DEFAULT_DESCRIPTION,
|
||||
typeIcon: "gear",
|
||||
typeLabel: "Business",
|
||||
category: business.categoryName || "Business",
|
||||
cover: business.bannerUrl || PLACEHOLDER_COVER,
|
||||
logo: business.logoUrl || PLACEHOLDER_LOGO,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_MARKETPLACE_API_URL?.replace(/\/$/, "") ||
|
||||
process.env.MARKETPLACE_API_URL?.replace(/\/$/, "") ||
|
||||
"http://localhost:3001/api/v1";
|
||||
|
||||
export type WebsiteMediaItem = {
|
||||
id: string;
|
||||
imageUrl: string;
|
||||
linkUrl: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
cta?: string;
|
||||
};
|
||||
|
||||
export type WebsiteHomeMedia = {
|
||||
slider: WebsiteMediaItem[];
|
||||
standingBanners: WebsiteMediaItem[];
|
||||
categories: WebsiteMediaItem[];
|
||||
menuImages: WebsiteMediaItem[];
|
||||
aboutUs: WebsiteMediaItem;
|
||||
banners: WebsiteMediaItem[];
|
||||
};
|
||||
|
||||
function resolveMediaUrl(url: string) {
|
||||
if (!url) return url;
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) return url;
|
||||
if (url.startsWith("/api/v1/")) {
|
||||
const origin = API_BASE_URL.replace(/\/api\/v1$/, "");
|
||||
return `${origin}${url}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function mapItem(item: WebsiteMediaItem): WebsiteMediaItem {
|
||||
return { ...item, imageUrl: resolveMediaUrl(item.imageUrl) };
|
||||
}
|
||||
|
||||
export async function getWebsiteHomeMedia(): Promise<WebsiteHomeMedia | null> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/website/home-media`, {
|
||||
next: { revalidate: 30 },
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const data = (await response.json()) as WebsiteHomeMedia;
|
||||
return {
|
||||
slider: (data.slider ?? []).map(mapItem),
|
||||
standingBanners: (data.standingBanners ?? []).map(mapItem),
|
||||
categories: (data.categories ?? []).map(mapItem),
|
||||
menuImages: (data.menuImages ?? []).map(mapItem),
|
||||
aboutUs: mapItem(data.aboutUs),
|
||||
banners: (data.banners ?? []).map(mapItem),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const dashboardUrl =
|
||||
process.env.NEXT_PUBLIC_DASHBOARD_URL?.replace(/\/$/, "") ||
|
||||
"http://localhost:3002";
|
||||
|
||||
const customerDashboardUrl =
|
||||
process.env.NEXT_PUBLIC_CUSTOMER_DASHBOARD_URL?.replace(/\/$/, "") ||
|
||||
"http://customer.localhost:3002";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: "/login",
|
||||
destination: `${customerDashboardUrl}/login`,
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/signup",
|
||||
destination: `${customerDashboardUrl}/login`,
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/forgot-password",
|
||||
destination: `${dashboardUrl}/forgot-password`,
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "iranbay-website",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "16.3.4",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.3.4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 339 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 611 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 627 KiB |
|
After Width: | Height: | Size: 371 KiB |
|
After Width: | Height: | Size: 876 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 703 KiB |
|
After Width: | Height: | Size: 703 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 793 KiB |
|
After Width: | Height: | Size: 315 KiB |
|
After Width: | Height: | Size: 508 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 119 KiB |