feat: Revert homepage changes, fix Persian slug 404, and make various article and about us adjustments

- Removed the newly added city-focused section from the homepage.
- Fixed 404 error for Persian slugs in specialties and blog pages by implementing a decode helper.
- Removed the "مخصوص شهر" label from article headers.
- Linked article tags to their respective filter pages.
- Adjusted related content images to prevent distortion.
- Added a section in the "About Us" page for physician registration guidance.
- Updated the logo in the "About Us" header to the correct version.
- Added tests for the new functionality and ensured existing tests are updated accordingly.
This commit is contained in:
hamed
2026-08-08 21:06:48 +03:30
parent e8a073cae6
commit 9cfbd2e002
18 changed files with 704 additions and 145 deletions
+4 -8
View File
@@ -1,15 +1,11 @@
import Image from "next/image";
import React from "react";
import SiteLogo from "@/components/common/SiteLogo";
function Head({ siteName, slogan }) {
function Head({ siteName, slogan, city }) {
return (
<div className="bg-[#5559C2] rounded-none md:rounded-lg w-full py-[24px] flex flex-col items-center justify-center gap-[13px] md:gap-[16px]">
<Image
src="/assets/images/logo-2.png"
height={46}
width={46}
alt="logo"
/>
{/* رنگ پیش‌فرض لوگو #5559CE است و روی همین پس‌زمینه گم می‌شود؛ نسخهٔ روشن. */}
<SiteLogo city={city} stroke="#FAFAFA" className="w-[46px] h-[46px]" />
<h1 className="text-[#FAFAFA] text-[16px] sm:text-[18px] md:text-[22px] lg:text-[24px] font-bold">
درباره {siteName}
</h1>
+46
View File
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import Head from "@/components/aboutUs/Head";
const CITY = { name: "یاسوج", site_name: "یاسوج نوبت", logo_url: null };
describe("سربرگ درباره ما", () => {
it("شهر بدون logo_url لوگوی درون‌خطی با رنگ روشن می‌گیرد", () => {
const { container } = render(
<Head siteName={CITY.site_name} slogan="کنار شما" city={CITY} />
);
const path = container.querySelector("svg path");
// پس‌زمینهٔ هدر #5559C2 است؛ رنگ پیش‌فرض لوگو روی آن دیده نمی‌شود.
expect(path).toHaveAttribute("stroke", "#FAFAFA");
});
it("شهر دارای logo_url همان تصویر را نشان می‌دهد", () => {
render(
<Head
siteName={CITY.site_name}
city={{ ...CITY, logo_url: "https://cdn.test/logo.png" }}
/>
);
expect(screen.getByRole("img", { name: "logo" })).toHaveAttribute(
"src",
"https://cdn.test/logo.png"
);
});
it("لوگوی قدیمی logo-2.png دیگر رندر نمی‌شود", () => {
const { container } = render(<Head siteName={CITY.site_name} city={CITY} />);
expect(container.innerHTML).not.toContain("logo-2");
});
it("شعار نداشتن، سربرگ را نمی‌شکند", () => {
render(<Head siteName={CITY.site_name} city={CITY} />);
expect(
screen.getByRole("heading", { name: `درباره ${CITY.site_name}` })
).toBeInTheDocument();
});
});
+17 -1
View File
@@ -11,7 +11,7 @@ async function AboutUsPage() {
<div
className=" pt-[24px] sm:pt-[56px] md:pt-[88px] lg:pt-[120px] mt-[56px] px-0 md:px-[80px] lg:px-[108px] "
>
<Head siteName={siteName} slogan={slogan} />
<Head siteName={siteName} slogan={slogan} city={matchedCity} />
<div
className=" text-[#525252] text-[16px] font-normal mt-[32px] leading-[28px] md:leading-[32px] lg:leading-[40px] px-[16px] sm:px-[52px] md:px-[15%] text-justify md:text-center flex flex-col gap-[16px] "
>
@@ -27,6 +27,22 @@ async function AboutUsPage() {
نوبت خود را ثبت کنید. هدف ما کاهش زمان انتظار و ایجاد ارتباط مؤثر میان
بیماران، پزشکان و کلینیکهاست.
</p>
<p>
اگر پزشک هستید و میخواهید در {siteName} نوبتدهی آنلاین داشته باشید،
ثبتنام از طریق سامانهی کلینیکپرو انجام میشود. حساب مطب خود را
آنجا بسازید؛ پس از تأیید، پروفایل و برنامهی نوبتدهی شما روی{" "}
{siteName} نمایش داده میشود.
</p>
<div className="flex justify-center mt-[8px]">
<a
href="https://clinic-pro.ir/"
target="_blank"
rel="noopener noreferrer"
className="px-[24px] py-[12px] rounded-lg bg-[#5559CE] border border-[#5559CE] text-[#FFF] text-[14px] md:text-[16px] font-medium hover:bg-[#FFF] hover:text-[#5559CE] transition-all duration-500"
>
ثبتنام پزشکان در کلینیکپرو
</a>
</div>
</div>
<Services />
</div>
+55
View File
@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import Head from "@/components/blog/head";
const BLOG = {
title: "آلرژی دارویی",
author: "دکتر رضایی",
created: 1750000000,
reading_time: 5,
tag: [{ name: "آلرژی" }, { name: "قلب و عروق" }],
};
describe("سربرگ مقاله", () => {
it("برچسب «مخصوص شهر» را نشان نمی‌دهد", () => {
render(<Head data={BLOG} />);
expect(screen.queryByText("مخصوص شهر:")).toBeNull();
});
it("نویسنده و تاریخ انتشار سر جایشان می‌مانند", () => {
render(<Head data={BLOG} />);
expect(screen.getByText("نویسنده:")).toBeInTheDocument();
expect(screen.getByText("تاریخ انتشار:")).toBeInTheDocument();
});
it("هر تگ به لیست مقاله‌های همان تگ لینک می‌شود", () => {
render(<Head data={BLOG} />);
const links = screen.getAllByRole("link", { name: "قلب و عروق" });
const url = new URL(links[0].getAttribute("href"), "https://yasuj-nobat.ir");
expect(links).toHaveLength(1);
expect(url.pathname).toBe("/blogs");
// قرارداد واقعی همان چیزی است که `searchParams.get("tag")` می‌خواند،
// نه شکل دقیق encode — شکل شیئیِ href فاصله را `+` می‌کند و بردکرامب `%20`.
expect(url.searchParams.get("tag")).toBe("قلب و عروق");
});
it("تگ حاوی & با پارامتر دوم اشتباه گرفته نمی‌شود", () => {
render(<Head data={{ ...BLOG, tag: [{ name: "قلب & عروق" }] }} />);
const links = screen.getAllByRole("link", { name: "قلب & عروق" });
const url = new URL(links[0].getAttribute("href"), "https://yasuj-nobat.ir");
expect(url.searchParams.get("tag")).toBe("قلب & عروق");
expect(Array.from(url.searchParams.keys())).toEqual(["tag"]);
});
it("مقالهٔ بدون تگ هیچ فهرست تگی رندر نمی‌کند", () => {
const { container } = render(<Head data={{ ...BLOG, tag: [] }} />);
expect(container.querySelector("ul")).toBeNull();
});
});
+10 -19
View File
@@ -4,7 +4,7 @@ import { convertTimestampToJalali } from "@/helper";
const crumbClass =
"text-[#9B9B9B] text-[11px] sm:text-[13px] md:text-[15px] lg:text-[16px] font-normal";
function Head({ data, cityName }) {
function Head({ data }) {
const firstTag = data?.tag?.[0];
const createdDate = data?.created ? convertTimestampToJalali(data.created) : "";
// normalizeBlog تگ‌ها را به [{name}] تبدیل می‌کند؛ نامِ خالی حذف می‌شود.
@@ -44,18 +44,6 @@ function Head({ data, cityName }) {
</h1>
</div>
<div className="flex mt-[16px] sm:mt-[18px] md:mt-[21px] lg:mt-[24px] items-center justify-start gap-[35px]">
{/* شهر پست در HTML قابل‌مشاهده — نه فقط در متادیتا. برچسب با همان
تایپوگرافی «نویسنده/تاریخ» تا با بقیهٔ سربرگ هم‌خانواده بماند. */}
{cityName && (
<div className="flex items-center justify-start gap-1">
<p className="text-[#9B9B9B] text-[11px] md:text-[13px] lg:text-[14px] font-normal">
مخصوص شهر:
</p>
<p className="text-[#9B9B9B] text-[11px] md:text-[14px] lg:text-[16px] font-medium">
{cityName}
</p>
</div>
)}
{data?.author && (
<div className="flex items-center justify-start gap-1">
<p className="text-[#9B9B9B] text-[11px] md:text-[13px] lg:text-[14px] font-normal">
@@ -88,15 +76,18 @@ function Head({ data, cityName }) {
)}
</div>
{/* فهرست کامل تگ‌ها — تا این تغییر فقط تگ اول در بردکرامب دیده می‌شد.
صفحهٔ فیلتر تگ وجود ندارد، پس چیپ‌ها بدون لینک رندر می‌شوند. */}
مقصد هر چیپ همان لیست مقاله‌هاست که `?tag=` را می‌خواند؛ شکل شیئیِ href
تگِ حاوی فاصله یا & را هم درست encode می‌کند. */}
{tags.length > 0 && (
<ul className="flex flex-wrap items-center justify-start gap-2 mt-[12px] md:mt-[16px]">
{tags.map((tag, idx) => (
<li
key={`${tag}-${idx}`}
className="bg-[#F5F5F5] text-[#525252] rounded-full px-[12px] py-[4px] text-[11px] md:text-[13px] font-medium"
>
{tag}
<li key={`${tag}-${idx}`}>
<Link
href={{ pathname: "/blogs", query: { tag } }}
className="block bg-[#F5F5F5] text-[#525252] rounded-full px-[12px] py-[4px] text-[11px] md:text-[13px] font-medium hover:bg-[#E8E8E8] hover:text-[#3B3B3B] transition-colors"
>
{tag}
</Link>
</li>
))}
</ul>
+2 -2
View File
@@ -2,10 +2,10 @@ import Detail from "./detail";
import Head from "./head";
import RelatedContent from "./relatedContent";
function BlogPage({ blog, blogs, cityName }) {
function BlogPage({ blog, blogs }) {
return (
<div className="padding-responsive pt-[84px] sm:pt-[110px] md:pt-[140px] lg:pt-[168px]">
<Head data={blog} cityName={cityName} />
<Head data={blog} />
<div className="flex flex-col gap-y-[32px] lg:flex-row items-start justify-center gap-[24px] mt-[16px] sm:mt-[19px] md:mt-[21px] lg:mt-[24px]">
<Detail data={blog} />
<RelatedContent data={blogs} />
+3 -1
View File
@@ -10,8 +10,10 @@ function Item({ data, idx, loading }) {
return (
<li className="flex w-full relative items-start justify-start gap-[8px] py-[12px]">
{/* CustomLoading ظرف نمی‌سازد، پس فرزند مستقیمِ فلکس همین لینک است؛
بدون shrink-0 تیتر بلند عرض عکس را می‌بلعد و ارتفاعش ثابت می‌ماند. */}
<CustomLoading width={80} height={72} loading={loading}>
<Link href={`/blog/${data.uuid}`}>
<Link href={`/blog/${data.uuid}`} className="block shrink-0">
<img
className=" w-[64px] sm:w-[73px] md:w-[85px] lg:w-[97px] h-[56px] sm:h-[64px] md:h-[75px] lg:h-[88px] rounded-[8px] overflow-hidden object-cover "
src={cover}
@@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import Item from "@/components/blog/relatedContent/Item";
const DATA = {
uuid: "8f1c0f2e-0000-4000-8000-000000000001",
title: "تیتری که بسیار بلند است و باید ستون متن را پر کند بدون آنکه عکس را باریک کند",
created: 1750000000,
};
describe("آیتم مطالب مرتبط", () => {
it("ظرف عکس در فلکس فشرده نمی‌شود", () => {
render(<Item data={DATA} idx={0} loading={false} />);
const image = screen.getByRole("img", { name: DATA.title });
// jsdom چیدمان را اندازه نمی‌گیرد؛ آنچه قابل تست است همین قرارداد کلاس است.
expect(image.closest("a")).toHaveClass("shrink-0");
});
it("ستون متن اجازهٔ کوچک شدن دارد تا سرریز نکند", () => {
const { container } = render(<Item data={DATA} idx={0} loading={false} />);
expect(container.querySelector("li > div")).toHaveClass("min-w-0");
});
});
+1 -1
View File
@@ -2,7 +2,7 @@
import { useEffect, useState } from "react";
import DoctorAvatar from "@/app/component/DoctorAvatar";
import QrImg from "./QrImg";
import SiteLogo from "./SiteLogo";
import SiteLogo from "@/components/common/SiteLogo";
import { getStateInfoClient } from "@/lib/getStateInfoClient";
import { posterSpecialtyLine, splitSpecialties } from "@/lib/specialtyDisplay";
import { doctorTitle } from "@/helper";
+1 -1
View File
@@ -2,7 +2,7 @@
import { useEffect, useState } from "react";
import DoctorAvatar from "@/app/component/DoctorAvatar";
import QrImg from "./QrImg";
import SiteLogoBase from "./SiteLogo";
import SiteLogoBase from "@/components/common/SiteLogo";
import { getStateInfoClient } from "@/lib/getStateInfoClient";
import { posterSpecialtyLine, splitSpecialties } from "@/lib/specialtyDisplay";
import { doctorTitle } from "@/helper";
-2
View File
@@ -3,7 +3,6 @@ import Image from "next/image";
import SearchBar from "./search";
import TextHeader from "./TextHeader";
import FrequentSearches from "./FrequentSearches";
import CityHighlights from "./CityHighlights";
function HomePage() {
return (
@@ -19,7 +18,6 @@ function HomePage() {
<TextHeader />
<SearchBar />
<FrequentSearches />
<CityHighlights />
</div>
</div>
);