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 { SiteShell } from "@/components/SiteShell";
import { ContactForm } from "@/components/ContactForm";
import { getBusinessInfo } from "@/lib/api/business";
import { getBusinessInfo, getPrimaryPhone, toPhoneText } from "@/lib/api/business";
import { site } from "@/data/home";
export const metadata: Metadata = {
@@ -11,10 +11,11 @@ export const metadata: Metadata = {
export default async function ContactPage() {
const info = await getBusinessInfo();
const phones =
info?.phoneNumbers?.filter(Boolean).length
? info.phoneNumbers
: [site.phoneDisplay];
const phones = (info?.phoneNumbers ?? [])
.map(toPhoneText)
.filter(Boolean);
const displayPhones =
phones.length > 0 ? phones : [getPrimaryPhone(info)];
const emails = info?.emails?.filter(Boolean) ?? [];
const addresses =
info?.addresses?.length
@@ -56,19 +57,16 @@ export default async function ContactPage() {
<div>
<p className="font-bold text-brand-ink">تلفن</p>
<div className="mt-1 space-y-1">
{phones.map((phone) => {
const phoneText = String(phone ?? "")
return (
{displayPhones.map((phone) => (
<a
key={phoneText}
href={`tel:${phoneText.replace(/\s/g, "")}`}
key={phone}
href={`tel:${phone.replace(/\s/g, "")}`}
className="latin-digits block text-brand"
dir="ltr"
>
{phoneText}
{phone}
</a>
)
})}
))}
</div>
</div>
+41 -7
View File
@@ -2,7 +2,7 @@
@font-face {
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-style: normal;
font-display: swap;
@@ -10,7 +10,7 @@
@font-face {
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-style: normal;
font-display: swap;
@@ -18,7 +18,7 @@
@font-face {
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-style: normal;
font-display: swap;
@@ -26,12 +26,44 @@
@font-face {
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-style: normal;
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 {
--background: #f2f5f8;
--foreground: #0f1720;
@@ -43,6 +75,7 @@
--brand-ink: #06233f;
--steel: #8fa3b8;
--container: 1180px;
--font-industrial: "Oswald", Impact, sans-serif;
}
@theme inline {
@@ -56,7 +89,8 @@
--color-brand-ink: var(--brand-ink);
--color-steel: var(--steel);
--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-family: var(--font-industrial), "Oswald", Impact, sans-serif;
font-family: var(--font-industrial);
unicode-bidi: isolate;
}
@@ -111,7 +145,7 @@ img {
}
.section-kicker {
font-family: var(--font-industrial), "Oswald", Impact, sans-serif;
font-family: var(--font-industrial);
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--brand);
+1 -8
View File
@@ -1,13 +1,6 @@
import type { Metadata, Viewport } from "next";
import { Oswald } from "next/font/google";
import "./globals.css";
const oswald = Oswald({
subsets: ["latin"],
variable: "--font-industrial",
weight: ["400", "500", "600", "700"],
});
export const metadata: Metadata = {
metadataBase: new URL("https://www.mashinify.com"),
title: {
@@ -47,7 +40,7 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
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>
</html>
);
+4 -2
View File
@@ -6,13 +6,15 @@ import { HeaderContainer } from "@/components/HeaderContainer";
import { Hero } from "@/components/Hero";
import { Products } from "@/components/Products";
import { SellCta } from "@/components/SellCta";
import { getPrimaryPhoneNumber } from "@/lib/api/business";
import { listBlogs } from "@/lib/api/blogs";
import { listUserProducts } from "@/lib/api/user-products";
export default async function Home() {
const [userProducts, blogs] = await Promise.all([
const [userProducts, blogs, phone] = await Promise.all([
listUserProducts({ page: 1, pageSize: 8 }),
listBlogs({ page: 1, pageSize: 3 }),
getPrimaryPhoneNumber(),
]);
return (
@@ -26,7 +28,7 @@ export default async function Home() {
<AboutCta />
<Articles items={blogs.items} total={blogs.total} />
</main>
<Footer />
<Footer phone={phone} />
</>
);
}
+5 -4
View File
@@ -6,7 +6,7 @@ import { ProductMarketplaceCtas } from "@/components/ProductMarketplaceCtas";
import { ProductTabs } from "@/components/ProductTabs";
import { Products } from "@/components/Products";
import { SiteShell } from "@/components/SiteShell";
import { site } from "@/data/home";
import { getPrimaryPhoneNumber } from "@/lib/api/business";
import {
formatCondition,
formatPrice,
@@ -58,7 +58,7 @@ export default async function UserProductDetailPage({ params }: PageProps) {
const description =
product.descriptionFa || product.description || product.descriptionEn;
const deliveryNotes = product.deliveryNotes ?? product.deliveryNote ?? null;
const [technicalInfo, similarResult] = await Promise.all([
const [technicalInfo, similarResult, phone] = await Promise.all([
getUserProductTechnicalInfo(slug),
product.categoryId
? listUserProducts({
@@ -66,6 +66,7 @@ export default async function UserProductDetailPage({ params }: PageProps) {
pageSize: 5,
})
: Promise.resolve({ items: [], total: 0 }),
getPrimaryPhoneNumber(),
]);
const similarProducts = similarResult.items
@@ -144,7 +145,7 @@ export default async function UserProductDetailPage({ params }: PageProps) {
)}
<div className="mt-8 flex justify-end">
<a href={`tel:${site.phone}`} className="btn-primary">
<a href={`tel:${phone}`} className="btn-primary">
تماس برای استعلام
</a>
</div>
@@ -177,7 +178,7 @@ export default async function UserProductDetailPage({ params }: PageProps) {
/>
) : null}
<ProductMarketplaceCtas />
<ProductMarketplaceCtas phone={phone} />
</div>
</article>
</SiteShell>
+7 -3
View File
@@ -1,6 +1,10 @@
import { footerLinks, site } from "@/data/home";
export function Footer() {
type FooterProps = {
phone?: string;
};
export function Footer({ phone = site.phone }: FooterProps) {
return (
<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">
@@ -15,11 +19,11 @@ export function Footer() {
<div className="mt-6 space-y-2 text-sm text-white/80">
<p>{site.address}</p>
<a
href={`tel:${site.phone}`}
href={`tel:${phone}`}
className="latin-digits inline-block text-brand"
dir="ltr"
>
{site.phoneDisplay}
{phone}
</a>
</div>
</div>
+46 -27
View File
@@ -21,6 +21,7 @@ type HeaderProps = {
forceSolid?: boolean;
productMegaMenu?: NavProductMegaMenuGroup[];
logoUrl?: string | null;
phone?: string;
};
function ChevronDown({ open }: { open?: boolean }) {
@@ -212,29 +213,46 @@ function ProductsNavDropdown({
</a>
</div>
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{groups.map((group) => (
<div key={group.id}>
<a
href={group.href}
className="block text-base font-extrabold !text-white transition hover:!text-brand"
>
{group.label}
</a>
<ul className="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
className={
groups.length === 1
? "max-w-4xl"
: "grid gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
}
>
{groups.map((group) => {
const useTwoColumns =
groups.length === 1 || group.items.length > 8;
return (
<div key={group.id}>
<a
href={group.href}
className="block text-base font-extrabold !text-white transition hover:!text-brand"
>
{group.label}
</a>
<ul
className={
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>
@@ -247,6 +265,7 @@ export function Header({
forceSolid = false,
productMegaMenu = [],
logoUrl = null,
phone = site.phone,
}: HeaderProps) {
const [open, setOpen] = useState(false);
const [scrolled, setScrolled] = useState(forceSolid);
@@ -355,11 +374,11 @@ export function Header({
<div className="hidden items-center gap-4 lg:flex">
<a
href={`tel:${site.phone}`}
href={`tel:${phone}`}
className="latin-digits text-sm !text-white/85 transition hover:!text-white"
dir="ltr"
>
{site.phoneDisplay}
{phone}
</a>
<a
href={site.customerLoginUrl}
@@ -401,11 +420,11 @@ export function Header({
<nav className="container-page flex flex-col gap-1 py-4">
{navItems.map(renderMobileNavItem)}
<a
href={`tel:${site.phone}`}
href={`tel:${phone}`}
className="latin-digits mt-2 px-2 py-3 !text-brand"
dir="ltr"
>
{site.phoneDisplay}
{phone}
</a>
</nav>
</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 { Header } from "@/components/Header";
@@ -17,6 +17,7 @@ export async function HeaderContainer({ forceSolid = false }: HeaderContainerPro
forceSolid={forceSolid}
productMegaMenu={productMegaMenu}
logoUrl={businessInfo?.logoUrl ?? null}
phone={getPrimaryPhone(businessInfo)}
/>
);
}
+9 -3
View File
@@ -1,7 +1,13 @@
import Link from "next/link";
import { site } from "@/data/home";
export function ProductMarketplaceCtas() {
type ProductMarketplaceCtasProps = {
phone?: string;
};
export function ProductMarketplaceCtas({
phone = site.phone,
}: ProductMarketplaceCtasProps) {
return (
<div className="mt-12 space-y-4">
<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>
<a href={`tel:${site.phone}`} className="btn-ghost !min-h-[3rem]">
{site.phoneDisplay}
<a href={`tel:${phone}`} className="btn-ghost !min-h-[3rem]">
{phone}
</a>
</div>
</div>
+5 -2
View File
@@ -1,6 +1,7 @@
import { Footer } from "@/components/Footer";
import { HeaderContainer } from "@/components/HeaderContainer";
import { PageBanner } from "@/components/PageBanner";
import { getPrimaryPhoneNumber } from "@/lib/api/business";
type SiteShellProps = {
children: React.ReactNode;
@@ -12,11 +13,13 @@ type SiteShellProps = {
};
};
export function SiteShell({
export async function SiteShell({
children,
solidHeader = true,
banner,
}: SiteShellProps) {
const phone = await getPrimaryPhoneNumber();
return (
<>
<HeaderContainer forceSolid={solidHeader} />
@@ -30,7 +33,7 @@ export function SiteShell({
) : null}
{children}
</main>
<Footer />
<Footer phone={phone} />
</>
);
}
+2 -2
View File
@@ -4,8 +4,8 @@ export const site = {
tagline: "بازار ماشین‌آلات سنگین و صنعتی",
description:
"ماشینیفای، بستر خرید و فروش ماشین‌آلات صنعتی نو و کارکرده؛ از دستگاه‌های سیم و کابل تا لیزر و اکسترودر.",
phone: "025-36700000",
phoneDisplay: "025-36700000",
phone: "09122516248",
phoneDisplay: "09122516248",
address: "قم، سالاریه، میدان پیچک، ساختمان بنفشه",
loginUrl: "https://app.mashinify.com/auth/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 type { BusinessInfo } from "./types";
import type { BusinessInfo, BusinessPhoneNumber } from "./types";
export async function getBusinessInfo() {
try {
@@ -9,3 +10,22 @@ export async function getBusinessInfo() {
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[];
};
export type BusinessPhoneNumber = {
label?: string | null;
type?: string | null;
number: string;
};
export type BusinessInfo = {
id: string;
name: string;
@@ -109,7 +115,8 @@ export type BusinessInfo = {
logoUrl: string | null;
faviconUrl: string | null;
emails: string[];
phoneNumbers: string[];
/** API returns objects; older payloads may use plain strings. */
phoneNumbers: Array<string | BusinessPhoneNumber>;
socialMedia?: {
whatsapp?: string;
telegram?: string;
@@ -123,6 +130,9 @@ export type BusinessInfo = {
address?: string | null;
cityName?: string | null;
cityNameFa?: string | null;
city?: string | null;
province?: string | null;
landline?: string | null;
}>;
};