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
+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;
}>;
};