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:
co-authored by
Cursor
parent
d249d958eb
commit
b36c07e28f
+11
-13
@@ -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
@@ -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
@@ -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
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user