feat(projects): صفحهبندی شمارهدار بهجای «مشاهده بیشتر» — ۱۲ پروژه در هر صفحه، با برگشت نگاه به ابتدای فهرست
This commit is contained in:
@@ -12,6 +12,9 @@ export const fa = {
|
||||
switchLang: 'English',
|
||||
switchLangAria: 'تغییر زبان به انگلیسی',
|
||||
understood: 'متوجه شدم',
|
||||
paginationAria: 'صفحهبندی پروژهها',
|
||||
previousPage: 'صفحهٔ قبل',
|
||||
nextPage: 'صفحهٔ بعد',
|
||||
|
||||
// هدر و ناوبری
|
||||
mainNav: 'ناوبری اصلی',
|
||||
@@ -141,6 +144,9 @@ export const en: Dict = {
|
||||
switchLang: 'فا',
|
||||
switchLangAria: 'Switch language to Persian (Farsi)',
|
||||
understood: 'Got it',
|
||||
paginationAria: 'Projects pagination',
|
||||
previousPage: 'Previous page',
|
||||
nextPage: 'Next page',
|
||||
|
||||
mainNav: 'Main navigation',
|
||||
mobileNav: 'Mobile navigation',
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { PageHero } from '../components/PageHero';
|
||||
import { Reveal } from '../components/Reveal';
|
||||
import { ProjectCard } from '../components/ProjectCard';
|
||||
import { PremiumButton } from '../components/PremiumButton';
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
||||
import { toPersianDigits } from '../utils/digits';
|
||||
import { ContactSection } from '../components/ContactSection';
|
||||
import { useSite, useStatusLabel } from '../api';
|
||||
import { useLang } from '../i18n/lang';
|
||||
@@ -14,8 +15,10 @@ import { MAP_PROJECTS, type MapProject } from '../data/mapProjects';
|
||||
|
||||
export function Projects() {
|
||||
const { status_labels, projects: dbProjects } = useSite();
|
||||
const { t } = useLang();
|
||||
const { t, lang } = useLang();
|
||||
const save = useFieldSaver();
|
||||
/** لنگر ابتدای فهرست — پس از تغییر صفحه، نگاه کاربر به بالا برمیگردد */
|
||||
const listTopRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const saveStatusLabel = (key: string, value: string) => {
|
||||
const label = status_labels.find((s) => s.key === key);
|
||||
@@ -38,9 +41,8 @@ export function Projects() {
|
||||
// فایل استاتیک فقط وقتی دیتابیس خالی است جانشین میشود (محتوای قدیمی).
|
||||
const cardProjects = useMemo(() => dbProjects, [dbProjects]);
|
||||
|
||||
// Filtering and Pagination State
|
||||
// صفحهبندی شمارهدار — هر صفحه ۱۲ پروژه
|
||||
const [page, setPage] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const itemsPerPage = 12;
|
||||
|
||||
// Filter projects by status
|
||||
@@ -48,27 +50,32 @@ export function Projects() {
|
||||
return cardProjects.filter((p) => p.status === status);
|
||||
}, [cardProjects, status]);
|
||||
|
||||
// Currently displayed projects
|
||||
// پروژههای همین صفحه
|
||||
const totalPages = Math.max(1, Math.ceil(filteredProjects.length / itemsPerPage));
|
||||
const displayedProjects = useMemo(() => {
|
||||
return filteredProjects.slice(0, page * itemsPerPage);
|
||||
const start = (page - 1) * itemsPerPage;
|
||||
return filteredProjects.slice(start, start + itemsPerPage);
|
||||
}, [filteredProjects, page]);
|
||||
|
||||
// با تغییر فیلتر یا کوتاهشدن فهرست، صفحه به بازهٔ معتبر برگردد
|
||||
useEffect(() => {
|
||||
if (page > totalPages) setPage(totalPages);
|
||||
}, [page, totalPages]);
|
||||
|
||||
/** رفتن به صفحه و بازگرداندن نگاه کاربر به ابتدای فهرست */
|
||||
const goToPage = (next: number) => {
|
||||
const target = Math.min(Math.max(1, next), totalPages);
|
||||
if (target === page) return;
|
||||
setPage(target);
|
||||
listTopRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
};
|
||||
|
||||
// Handle Tab Switch
|
||||
const handleTabSwitch = (newStatus: 'done' | 'progress') => {
|
||||
setStatus(newStatus);
|
||||
setPage(1); // Reset pagination on tab switch
|
||||
};
|
||||
|
||||
// Handle Load More
|
||||
const handleLoadMore = () => {
|
||||
setIsLoading(true);
|
||||
// Brief loading state
|
||||
setTimeout(() => {
|
||||
setPage((prev) => prev + 1);
|
||||
setIsLoading(false);
|
||||
}, 400);
|
||||
};
|
||||
|
||||
const BASE_IMG_URL = "/seed-media/2026/09/";
|
||||
|
||||
// Resolve a project href from a MapProject entry by matching names — first in the
|
||||
@@ -112,6 +119,8 @@ export function Projects() {
|
||||
<section className="bg-ink py-24 lg:py-32" aria-label={t('projectsListAria')}>
|
||||
<div className="mx-auto max-w-site px-6 lg:px-10">
|
||||
|
||||
<div ref={listTopRef} aria-hidden="true" />
|
||||
|
||||
{/* دکمههای فیلتر */}
|
||||
<div className="flex items-center justify-center gap-4 md:gap-6" role="tablist" aria-label={t('projectFilterAria')}>
|
||||
{FILTERS.map((filter, index) => {
|
||||
@@ -167,17 +176,47 @@ export function Projects() {
|
||||
<AddItemButton resource="projects" editHrefBase="/admin/projects" />
|
||||
</div>
|
||||
|
||||
{/* Load More Button */}
|
||||
{displayedProjects.length < filteredProjects.length && (
|
||||
<div className="mt-16 flex justify-center">
|
||||
<PremiumButton
|
||||
onClick={handleLoadMore}
|
||||
disabled={isLoading}
|
||||
className="px-10 py-4 w-48"
|
||||
{/* صفحهبندی شمارهدار */}
|
||||
{totalPages > 1 && (
|
||||
<nav className="mt-16 flex flex-wrap items-center justify-center gap-2" aria-label={t('paginationAria')}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => goToPage(page - 1)}
|
||||
disabled={page === 1}
|
||||
aria-label={t('previousPage')}
|
||||
className="flex h-10 items-center gap-1.5 border border-ivory/20 px-4 text-xs font-light text-ivory/75 transition-colors duration-200 hover:border-copper hover:text-copper disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:border-ivory/20 disabled:hover:text-ivory/75"
|
||||
>
|
||||
{isLoading ? 'در حال بارگذاری...' : 'مشاهده بیشتر'}
|
||||
</PremiumButton>
|
||||
</div>
|
||||
<ChevronRightIcon className="h-3.5 w-3.5 rtl:rotate-0 ltr:rotate-180" />
|
||||
{t('previousPage')}
|
||||
</button>
|
||||
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((n) => (
|
||||
<button
|
||||
key={n}
|
||||
type="button"
|
||||
onClick={() => goToPage(n)}
|
||||
aria-current={n === page ? 'page' : undefined}
|
||||
className={`flex h-10 w-10 items-center justify-center border text-xs font-bold transition-colors duration-200 ${
|
||||
n === page
|
||||
? 'border-copper bg-copper/15 text-copper'
|
||||
: 'border-ivory/20 text-ivory/70 hover:border-copper hover:text-copper'
|
||||
}`}
|
||||
>
|
||||
{lang === 'fa' ? toPersianDigits(String(n)) : n}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => goToPage(page + 1)}
|
||||
disabled={page === totalPages}
|
||||
aria-label={t('nextPage')}
|
||||
className="flex h-10 items-center gap-1.5 border border-ivory/20 px-4 text-xs font-light text-ivory/75 transition-colors duration-200 hover:border-copper hover:text-copper disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:border-ivory/20 disabled:hover:text-ivory/75"
|
||||
>
|
||||
{t('nextPage')}
|
||||
<ChevronLeftIcon className="h-3.5 w-3.5 rtl:rotate-0 ltr:rotate-180" />
|
||||
</button>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user