Use local fonts and API primary phone across the site.

Serve Iran Yekan and Oswald from public/fonts, drive contact numbers from business-info phoneNumbers, and tighten the products mega menu with two-column lists.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-15 15:27:27 +03:30
co-authored by Cursor
parent d249d958eb
commit b36c07e28f
21 changed files with 165 additions and 74 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11 -13
View File
@@ -1,7 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { SiteShell } from "@/components/SiteShell"; import { SiteShell } from "@/components/SiteShell";
import { ContactForm } from "@/components/ContactForm"; import { ContactForm } from "@/components/ContactForm";
import { getBusinessInfo } from "@/lib/api/business"; import { getBusinessInfo, getPrimaryPhone, toPhoneText } from "@/lib/api/business";
import { site } from "@/data/home"; import { site } from "@/data/home";
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -11,10 +11,11 @@ export const metadata: Metadata = {
export default async function ContactPage() { export default async function ContactPage() {
const info = await getBusinessInfo(); const info = await getBusinessInfo();
const phones = const phones = (info?.phoneNumbers ?? [])
info?.phoneNumbers?.filter(Boolean).length .map(toPhoneText)
? info.phoneNumbers .filter(Boolean);
: [site.phoneDisplay]; const displayPhones =
phones.length > 0 ? phones : [getPrimaryPhone(info)];
const emails = info?.emails?.filter(Boolean) ?? []; const emails = info?.emails?.filter(Boolean) ?? [];
const addresses = const addresses =
info?.addresses?.length info?.addresses?.length
@@ -56,19 +57,16 @@ export default async function ContactPage() {
<div> <div>
<p className="font-bold text-brand-ink">تلفن</p> <p className="font-bold text-brand-ink">تلفن</p>
<div className="mt-1 space-y-1"> <div className="mt-1 space-y-1">
{phones.map((phone) => { {displayPhones.map((phone) => (
const phoneText = String(phone ?? "")
return (
<a <a
key={phoneText} key={phone}
href={`tel:${phoneText.replace(/\s/g, "")}`} href={`tel:${phone.replace(/\s/g, "")}`}
className="latin-digits block text-brand" className="latin-digits block text-brand"
dir="ltr" dir="ltr"
> >
{phoneText} {phone}
</a> </a>
) ))}
})}
</div> </div>
</div> </div>
+41 -7
View File
@@ -2,7 +2,7 @@
@font-face { @font-face {
font-family: "iran-yekan"; font-family: "iran-yekan";
src: url("https://cdn.meshkee.com/web-builder/fonts/iran-yekan/iran-yekan-regular.ttf"); src: url("/fonts/iran-yekan-regular.ttf") format("truetype");
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -10,7 +10,7 @@
@font-face { @font-face {
font-family: "iran-yekan"; font-family: "iran-yekan";
src: url("https://cdn.meshkee.com/web-builder/fonts/iran-yekan/iran-yekan-medium.ttf"); src: url("/fonts/iran-yekan-medium.ttf") format("truetype");
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -18,7 +18,7 @@
@font-face { @font-face {
font-family: "iran-yekan"; font-family: "iran-yekan";
src: url("https://cdn.meshkee.com/web-builder/fonts/iran-yekan/iran-yekan-bold.ttf"); src: url("/fonts/iran-yekan-bold.ttf") format("truetype");
font-weight: 700; font-weight: 700;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -26,12 +26,44 @@
@font-face { @font-face {
font-family: "iran-yekan"; font-family: "iran-yekan";
src: url("https://cdn.meshkee.com/web-builder/fonts/iran-yekan/iran-yekan-extrabold.ttf"); src: url("/fonts/iran-yekan-extrabold.ttf") format("truetype");
font-weight: 800; font-weight: 800;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@font-face {
font-family: "Oswald";
src: url("/fonts/oswald-400.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Oswald";
src: url("/fonts/oswald-500.ttf") format("truetype");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Oswald";
src: url("/fonts/oswald-600.ttf") format("truetype");
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Oswald";
src: url("/fonts/oswald-700.ttf") format("truetype");
font-weight: 700;
font-style: normal;
font-display: swap;
}
:root { :root {
--background: #f2f5f8; --background: #f2f5f8;
--foreground: #0f1720; --foreground: #0f1720;
@@ -43,6 +75,7 @@
--brand-ink: #06233f; --brand-ink: #06233f;
--steel: #8fa3b8; --steel: #8fa3b8;
--container: 1180px; --container: 1180px;
--font-industrial: "Oswald", Impact, sans-serif;
} }
@theme inline { @theme inline {
@@ -56,7 +89,8 @@
--color-brand-ink: var(--brand-ink); --color-brand-ink: var(--brand-ink);
--color-steel: var(--steel); --color-steel: var(--steel);
--font-sans: "iran-yekan", Tahoma, sans-serif; --font-sans: "iran-yekan", Tahoma, sans-serif;
--font-display: var(--font-industrial), "Oswald", Impact, sans-serif; --font-display: "Oswald", Impact, sans-serif;
--font-industrial: "Oswald", Impact, sans-serif;
} }
* { * {
@@ -83,7 +117,7 @@ body {
} }
.font-industrial { .font-industrial {
font-family: var(--font-industrial), "Oswald", Impact, sans-serif; font-family: var(--font-industrial);
unicode-bidi: isolate; unicode-bidi: isolate;
} }
@@ -111,7 +145,7 @@ img {
} }
.section-kicker { .section-kicker {
font-family: var(--font-industrial), "Oswald", Impact, sans-serif; font-family: var(--font-industrial);
letter-spacing: 0.18em; letter-spacing: 0.18em;
text-transform: uppercase; text-transform: uppercase;
color: var(--brand); color: var(--brand);
+1 -8
View File
@@ -1,13 +1,6 @@
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import { Oswald } from "next/font/google";
import "./globals.css"; import "./globals.css";
const oswald = Oswald({
subsets: ["latin"],
variable: "--font-industrial",
weight: ["400", "500", "600", "700"],
});
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL("https://www.mashinify.com"), metadataBase: new URL("https://www.mashinify.com"),
title: { title: {
@@ -47,7 +40,7 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="fa" dir="rtl" className={`${oswald.variable} h-full`}> <html lang="fa" dir="rtl" className="h-full">
<body className="min-h-full antialiased">{children}</body> <body className="min-h-full antialiased">{children}</body>
</html> </html>
); );
+4 -2
View File
@@ -6,13 +6,15 @@ import { HeaderContainer } from "@/components/HeaderContainer";
import { Hero } from "@/components/Hero"; import { Hero } from "@/components/Hero";
import { Products } from "@/components/Products"; import { Products } from "@/components/Products";
import { SellCta } from "@/components/SellCta"; import { SellCta } from "@/components/SellCta";
import { getPrimaryPhoneNumber } from "@/lib/api/business";
import { listBlogs } from "@/lib/api/blogs"; import { listBlogs } from "@/lib/api/blogs";
import { listUserProducts } from "@/lib/api/user-products"; import { listUserProducts } from "@/lib/api/user-products";
export default async function Home() { export default async function Home() {
const [userProducts, blogs] = await Promise.all([ const [userProducts, blogs, phone] = await Promise.all([
listUserProducts({ page: 1, pageSize: 8 }), listUserProducts({ page: 1, pageSize: 8 }),
listBlogs({ page: 1, pageSize: 3 }), listBlogs({ page: 1, pageSize: 3 }),
getPrimaryPhoneNumber(),
]); ]);
return ( return (
@@ -26,7 +28,7 @@ export default async function Home() {
<AboutCta /> <AboutCta />
<Articles items={blogs.items} total={blogs.total} /> <Articles items={blogs.items} total={blogs.total} />
</main> </main>
<Footer /> <Footer phone={phone} />
</> </>
); );
} }
+5 -4
View File
@@ -6,7 +6,7 @@ import { ProductMarketplaceCtas } from "@/components/ProductMarketplaceCtas";
import { ProductTabs } from "@/components/ProductTabs"; import { ProductTabs } from "@/components/ProductTabs";
import { Products } from "@/components/Products"; import { Products } from "@/components/Products";
import { SiteShell } from "@/components/SiteShell"; import { SiteShell } from "@/components/SiteShell";
import { site } from "@/data/home"; import { getPrimaryPhoneNumber } from "@/lib/api/business";
import { import {
formatCondition, formatCondition,
formatPrice, formatPrice,
@@ -58,7 +58,7 @@ export default async function UserProductDetailPage({ params }: PageProps) {
const description = const description =
product.descriptionFa || product.description || product.descriptionEn; product.descriptionFa || product.description || product.descriptionEn;
const deliveryNotes = product.deliveryNotes ?? product.deliveryNote ?? null; const deliveryNotes = product.deliveryNotes ?? product.deliveryNote ?? null;
const [technicalInfo, similarResult] = await Promise.all([ const [technicalInfo, similarResult, phone] = await Promise.all([
getUserProductTechnicalInfo(slug), getUserProductTechnicalInfo(slug),
product.categoryId product.categoryId
? listUserProducts({ ? listUserProducts({
@@ -66,6 +66,7 @@ export default async function UserProductDetailPage({ params }: PageProps) {
pageSize: 5, pageSize: 5,
}) })
: Promise.resolve({ items: [], total: 0 }), : Promise.resolve({ items: [], total: 0 }),
getPrimaryPhoneNumber(),
]); ]);
const similarProducts = similarResult.items const similarProducts = similarResult.items
@@ -144,7 +145,7 @@ export default async function UserProductDetailPage({ params }: PageProps) {
)} )}
<div className="mt-8 flex justify-end"> <div className="mt-8 flex justify-end">
<a href={`tel:${site.phone}`} className="btn-primary"> <a href={`tel:${phone}`} className="btn-primary">
تماس برای استعلام تماس برای استعلام
</a> </a>
</div> </div>
@@ -177,7 +178,7 @@ export default async function UserProductDetailPage({ params }: PageProps) {
/> />
) : null} ) : null}
<ProductMarketplaceCtas /> <ProductMarketplaceCtas phone={phone} />
</div> </div>
</article> </article>
</SiteShell> </SiteShell>
+7 -3
View File
@@ -1,6 +1,10 @@
import { footerLinks, site } from "@/data/home"; import { footerLinks, site } from "@/data/home";
export function Footer() { type FooterProps = {
phone?: string;
};
export function Footer({ phone = site.phone }: FooterProps) {
return ( return (
<footer id="contact" className="bg-brand-ink text-white"> <footer id="contact" className="bg-brand-ink text-white">
<div className="container-page grid gap-10 py-14 md:grid-cols-2 lg:grid-cols-4"> <div className="container-page grid gap-10 py-14 md:grid-cols-2 lg:grid-cols-4">
@@ -15,11 +19,11 @@ export function Footer() {
<div className="mt-6 space-y-2 text-sm text-white/80"> <div className="mt-6 space-y-2 text-sm text-white/80">
<p>{site.address}</p> <p>{site.address}</p>
<a <a
href={`tel:${site.phone}`} href={`tel:${phone}`}
className="latin-digits inline-block text-brand" className="latin-digits inline-block text-brand"
dir="ltr" dir="ltr"
> >
{site.phoneDisplay} {phone}
</a> </a>
</div> </div>
</div> </div>
+46 -27
View File
@@ -21,6 +21,7 @@ type HeaderProps = {
forceSolid?: boolean; forceSolid?: boolean;
productMegaMenu?: NavProductMegaMenuGroup[]; productMegaMenu?: NavProductMegaMenuGroup[];
logoUrl?: string | null; logoUrl?: string | null;
phone?: string;
}; };
function ChevronDown({ open }: { open?: boolean }) { function ChevronDown({ open }: { open?: boolean }) {
@@ -212,29 +213,46 @@ function ProductsNavDropdown({
</a> </a>
</div> </div>
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"> <div
{groups.map((group) => ( className={
<div key={group.id}> groups.length === 1
<a ? "max-w-4xl"
href={group.href} : "grid gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
className="block text-base font-extrabold !text-white transition hover:!text-brand" }
> >
{group.label} {groups.map((group) => {
</a> const useTwoColumns =
<ul className="mt-3 space-y-1.5"> groups.length === 1 || group.items.length > 8;
{group.items.map((item) => (
<li key={item.id}> return (
<a <div key={group.id}>
href={item.href} <a
className="block py-1 text-sm !text-white/75 transition hover:!text-white" href={group.href}
> className="block text-base font-extrabold !text-white transition hover:!text-brand"
{item.label} >
</a> {group.label}
</li> </a>
))} <ul
</ul> className={
</div> useTwoColumns
))} ? "mt-3 grid grid-cols-2 gap-x-10 gap-y-1.5"
: "mt-3 space-y-1.5"
}
>
{group.items.map((item) => (
<li key={item.id}>
<a
href={item.href}
className="block py-1 text-sm !text-white/75 transition hover:!text-white"
>
{item.label}
</a>
</li>
))}
</ul>
</div>
);
})}
</div> </div>
</div> </div>
</div> </div>
@@ -247,6 +265,7 @@ export function Header({
forceSolid = false, forceSolid = false,
productMegaMenu = [], productMegaMenu = [],
logoUrl = null, logoUrl = null,
phone = site.phone,
}: HeaderProps) { }: HeaderProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [scrolled, setScrolled] = useState(forceSolid); const [scrolled, setScrolled] = useState(forceSolid);
@@ -355,11 +374,11 @@ export function Header({
<div className="hidden items-center gap-4 lg:flex"> <div className="hidden items-center gap-4 lg:flex">
<a <a
href={`tel:${site.phone}`} href={`tel:${phone}`}
className="latin-digits text-sm !text-white/85 transition hover:!text-white" className="latin-digits text-sm !text-white/85 transition hover:!text-white"
dir="ltr" dir="ltr"
> >
{site.phoneDisplay} {phone}
</a> </a>
<a <a
href={site.customerLoginUrl} href={site.customerLoginUrl}
@@ -401,11 +420,11 @@ export function Header({
<nav className="container-page flex flex-col gap-1 py-4"> <nav className="container-page flex flex-col gap-1 py-4">
{navItems.map(renderMobileNavItem)} {navItems.map(renderMobileNavItem)}
<a <a
href={`tel:${site.phone}`} href={`tel:${phone}`}
className="latin-digits mt-2 px-2 py-3 !text-brand" className="latin-digits mt-2 px-2 py-3 !text-brand"
dir="ltr" dir="ltr"
> >
{site.phoneDisplay} {phone}
</a> </a>
</nav> </nav>
</div> </div>
+2 -1
View File
@@ -1,4 +1,4 @@
import { getBusinessInfo } from "@/lib/api/business"; import { getBusinessInfo, getPrimaryPhone } from "@/lib/api/business";
import { getProductMegaMenuGroups } from "@/lib/api/categories"; import { getProductMegaMenuGroups } from "@/lib/api/categories";
import { Header } from "@/components/Header"; import { Header } from "@/components/Header";
@@ -17,6 +17,7 @@ export async function HeaderContainer({ forceSolid = false }: HeaderContainerPro
forceSolid={forceSolid} forceSolid={forceSolid}
productMegaMenu={productMegaMenu} productMegaMenu={productMegaMenu}
logoUrl={businessInfo?.logoUrl ?? null} logoUrl={businessInfo?.logoUrl ?? null}
phone={getPrimaryPhone(businessInfo)}
/> />
); );
} }
+9 -3
View File
@@ -1,7 +1,13 @@
import Link from "next/link"; import Link from "next/link";
import { site } from "@/data/home"; import { site } from "@/data/home";
export function ProductMarketplaceCtas() { type ProductMarketplaceCtasProps = {
phone?: string;
};
export function ProductMarketplaceCtas({
phone = site.phone,
}: ProductMarketplaceCtasProps) {
return ( return (
<div className="mt-12 space-y-4"> <div className="mt-12 space-y-4">
<section className="relative overflow-hidden border border-line bg-surface"> <section className="relative overflow-hidden border border-line bg-surface">
@@ -61,8 +67,8 @@ export function ProductMarketplaceCtas() {
<Link href="/contact" className="btn-primary !bg-white !text-brand-ink hover:!bg-white/90"> <Link href="/contact" className="btn-primary !bg-white !text-brand-ink hover:!bg-white/90">
درخواست جستجو درخواست جستجو
</Link> </Link>
<a href={`tel:${site.phone}`} className="btn-ghost !min-h-[3rem]"> <a href={`tel:${phone}`} className="btn-ghost !min-h-[3rem]">
{site.phoneDisplay} {phone}
</a> </a>
</div> </div>
</div> </div>
+5 -2
View File
@@ -1,6 +1,7 @@
import { Footer } from "@/components/Footer"; import { Footer } from "@/components/Footer";
import { HeaderContainer } from "@/components/HeaderContainer"; import { HeaderContainer } from "@/components/HeaderContainer";
import { PageBanner } from "@/components/PageBanner"; import { PageBanner } from "@/components/PageBanner";
import { getPrimaryPhoneNumber } from "@/lib/api/business";
type SiteShellProps = { type SiteShellProps = {
children: React.ReactNode; children: React.ReactNode;
@@ -12,11 +13,13 @@ type SiteShellProps = {
}; };
}; };
export function SiteShell({ export async function SiteShell({
children, children,
solidHeader = true, solidHeader = true,
banner, banner,
}: SiteShellProps) { }: SiteShellProps) {
const phone = await getPrimaryPhoneNumber();
return ( return (
<> <>
<HeaderContainer forceSolid={solidHeader} /> <HeaderContainer forceSolid={solidHeader} />
@@ -30,7 +33,7 @@ export function SiteShell({
) : null} ) : null}
{children} {children}
</main> </main>
<Footer /> <Footer phone={phone} />
</> </>
); );
} }
+2 -2
View File
@@ -4,8 +4,8 @@ export const site = {
tagline: "بازار ماشین‌آلات سنگین و صنعتی", tagline: "بازار ماشین‌آلات سنگین و صنعتی",
description: description:
"ماشینیفای، بستر خرید و فروش ماشین‌آلات صنعتی نو و کارکرده؛ از دستگاه‌های سیم و کابل تا لیزر و اکسترودر.", "ماشینیفای، بستر خرید و فروش ماشین‌آلات صنعتی نو و کارکرده؛ از دستگاه‌های سیم و کابل تا لیزر و اکسترودر.",
phone: "025-36700000", phone: "09122516248",
phoneDisplay: "025-36700000", phoneDisplay: "09122516248",
address: "قم، سالاریه، میدان پیچک، ساختمان بنفشه", address: "قم، سالاریه، میدان پیچک، ساختمان بنفشه",
loginUrl: "https://app.mashinify.com/auth/login", loginUrl: "https://app.mashinify.com/auth/login",
customerLoginUrl: "https://customer.mashinify.com/login", customerLoginUrl: "https://customer.mashinify.com/login",
+21 -1
View File
@@ -1,5 +1,6 @@
import { site } from "@/data/home";
import { apiGet } from "./client"; import { apiGet } from "./client";
import type { BusinessInfo } from "./types"; import type { BusinessInfo, BusinessPhoneNumber } from "./types";
export async function getBusinessInfo() { export async function getBusinessInfo() {
try { try {
@@ -9,3 +10,22 @@ export async function getBusinessInfo() {
return null; return null;
} }
} }
export function toPhoneText(phone: string | BusinessPhoneNumber | null | undefined) {
if (!phone) return "";
if (typeof phone === "string") return phone.trim();
return String(phone.number ?? "").trim();
}
/** First entry in business `phoneNumbers`, or site fallback. */
export function getPrimaryPhone(
info: BusinessInfo | null | undefined,
fallback: string = site.phone,
) {
const first = info?.phoneNumbers?.[0];
return toPhoneText(first) || fallback;
}
export async function getPrimaryPhoneNumber() {
return getPrimaryPhone(await getBusinessInfo());
}
+11 -1
View File
@@ -100,6 +100,12 @@ export type CategoryTreeNode = Category & {
children: CategoryTreeNode[]; children: CategoryTreeNode[];
}; };
export type BusinessPhoneNumber = {
label?: string | null;
type?: string | null;
number: string;
};
export type BusinessInfo = { export type BusinessInfo = {
id: string; id: string;
name: string; name: string;
@@ -109,7 +115,8 @@ export type BusinessInfo = {
logoUrl: string | null; logoUrl: string | null;
faviconUrl: string | null; faviconUrl: string | null;
emails: string[]; emails: string[];
phoneNumbers: string[]; /** API returns objects; older payloads may use plain strings. */
phoneNumbers: Array<string | BusinessPhoneNumber>;
socialMedia?: { socialMedia?: {
whatsapp?: string; whatsapp?: string;
telegram?: string; telegram?: string;
@@ -123,6 +130,9 @@ export type BusinessInfo = {
address?: string | null; address?: string | null;
cityName?: string | null; cityName?: string | null;
cityNameFa?: string | null; cityNameFa?: string | null;
city?: string | null;
province?: string | null;
landline?: string | null;
}>; }>;
}; };