Files
dkala-front/src/app/[name]/(Main)/about/AboutPage.tsx
T
2026-02-10 10:05:33 +03:30

334 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import AnimatedBottomSheet from '@/components/bottomsheet/AnimatedBottomSheet';
import EqualizerIcon from '@/components/icons/EqualizerIcon';
import TelegramIcon from '@/components/icons/TelegramIcon';
import TabContainer from '@/components/tab/TabContainer';
import { TabHeader } from '@/components/tab/TabHeader';
import Comment from '@/components/utils/Comment';
import RateBar from '@/components/utils/RateBar';
import useToggle from '@/hooks/helpers/useToggle';
import { ArrowDown2, CallCalling, Clock, Gallery, InfoCircle, Instagram, Location, Star1, Whatsapp } from 'iconsax-react';
import { useQueryState } from 'next-usequerystate';
import Image from 'next/image';
import React from 'react'
import { useGetAbout, useGetReviews, useGetSchedules } from './hooks/useAboutData';
import AboutSkeleton from './components/AboutSkeleton';
const sortings = [
'جدیدترین',
'قدیمی ترین'
]
function AboutPage() {
const { data: aboutData, isLoading: aboutLoading } = useGetAbout();
const { data: reviewsData, isLoading: reviewsLoading } = useGetReviews();
const { data: schedulesData, isLoading: schedulesLoading } = useGetSchedules();
const [sorting, setSorting] = useQueryState('sortBy', { defaultValue: '0' });
const { state: sortingModal, toggle: toggleSortingModal, set: setSortingModal } = useToggle();
const shop = aboutData?.data;
const schedules = schedulesData?.data || [];
const isLoading = aboutLoading || reviewsLoading || schedulesLoading;
if (isLoading) {
return <AboutSkeleton />;
}
const changeSorting = (index: number) => {
setSorting(() => String(index));
setSortingModal(false);
}
const formatTime = (time: string): string => {
return time.substring(0, 5);
};
const getDayName = (weekDay: number): string => {
const days = ['یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه', 'شنبه'];
return days[weekDay] || '';
};
const groupSchedulesByDay = () => {
const grouped: Record<number, typeof schedules> = {};
schedules
.filter(schedule => schedule.isActive)
.forEach(schedule => {
if (!grouped[schedule.weekDay]) {
grouped[schedule.weekDay] = [];
}
grouped[schedule.weekDay].push(schedule);
});
return grouped;
};
const getTodaySchedule = () => {
const today = new Date().getDay();
const todaySchedules = schedules.filter(schedule =>
schedule.isActive && schedule.weekDay === today
);
if (todaySchedules.length > 0) {
const firstSchedule = todaySchedules[0];
return `${formatTime(firstSchedule.openTime)} تا ${formatTime(firstSchedule.closeTime)}`;
}
return null;
};
const firstTab = () => {
if (!shop) return null;
return (
<section aria-labelledby="about-title" className='py-4'>
<section
className="bg-container rounded-container shadow-container p-4">
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-[25px]">
<div className="">
<h2 className='text-sm2 font-bold leading-5'>{shop.name}</h2>
{shop.establishedYear && (
<p className="text-sm2 leading-5 mt-4">تاسیس: {shop.establishedYear}</p>
)}
{shop.tagNames && shop.tagNames.length > 0 && (
<p className="text-sm2 leading-5 mt-2">نوع محصولات: {shop.tagNames.join('، ')}</p>
)}
</div>
{shop.logo && (
<div className="rounded-normal overflow-clip">
<Image
alt='logo'
src={shop.logo}
width={88}
height={88}
unoptimized
priority
/>
</div>
)}
</div>
{shop.description && (
<div className="mt-[23px]">
<h3 className="text-sm2 font-bold leading-5">درباره مجموعه</h3>
<p className="text-sm2 leading-5 mt-3 border-spacing-48" style={{ wordSpacing: '0.01em' }}>
{shop.description}
</p>
{shop.images && shop.images.length > 0 && (
<div className='inline-flex gap-2 mt-[23px] items-center'>
<Gallery size={20} className='stroke-disabled-text' />
<span className='text-sm2 text-disabled-text font-medium pt-0.5'>عکس های فروشگاه</span>
</div>
)}
</div>
)}
</section>
{(shop.phone || shop.telegram || shop.whatsapp || shop.instagram) && (
<section
className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center">
<h2 className='text-sm2 font-medium leading-5'>ارتباط</h2>
<div className='col-span-1 text-center flex justify-center gap-4'>
{shop.phone &&
<a href={`tel://${shop.phone}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<CallCalling className='stroke-foreground' size={24} />
</a>
}
{shop.telegram &&
<a href={`https://t.me/${shop.telegram}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<TelegramIcon className='stroke-foreground' width={24} height={24} />
</a>
}
{shop.whatsapp &&
<a href={`https://whatsapp.com/?phone=${shop.whatsapp}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<Whatsapp className='stroke-foreground' size={24} />
</a>
}
{shop.instagram &&
<a href={`https://instagram.com/${shop.instagram}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
<Instagram className='stroke-foreground' size={24} />
</a>
}
</div>
</section>
)}
{shop.address && (
<section className="bg-container rounded-container shadow-container px-4 pt-6 pb-6 mt-4">
<h2 className='text-sm2 font-medium leading-5'>آدرس</h2>
<p className='text-sm2 mt-[9px] leading-5'>{shop.address}</p>
{shop.latitude && shop.longitude && (
<div className='inline-flex gap-2 mt-[23px] items-center '>
<Location size={20} className='stroke-disabled-text' />
<span className='text-sm2 text-disabled-text '>موقعیت روی نقشه</span>
</div>
)}
</section>
)}
{schedules.length > 0 && getTodaySchedule() && (
<section
aria-label='ساعات کاری'
className="bg-container rounded-container shadow-container py-6 px-4 mt-4 flex justify-between items-center">
<div className="flex items-center gap-2 justify-start">
<Clock size={16} className='stroke-disabled-text' />
<div className='text-sm2 font-medium leading-5 mt-0.5 text-[#8C90A3]'>باز</div>
<div className='size-1.5 bg-[#D9D9D9] rounded-full'></div>
<div className='text-sm2 mt-0.5'>امروز از ساعت {getTodaySchedule()}</div>
</div>
<div className="">
<ArrowDown2 size={16} className='stroke-[#292D32]' />
</div>
</section>
)}
{schedules.length > 0 && (
<section>
{Object.entries(groupSchedulesByDay())
.sort(([a], [b]) => Number(a) - Number(b))
.map(([weekDay, daySchedules]) => (
<div
key={weekDay}
style={{ boxShadow: '0px 0px 14px 0px #0000000F' }}
className="bg-[#F6F6FA] dark:bg-border rounded-container leading-5 py-6 px-4 mt-4 flex justify-between items-center">
<div className="text-sm2">
{getDayName(Number(weekDay))}
</div>
<div className='text-sm2 font-light'>
{daySchedules.map((schedule, index) => (
<span key={schedule.id}>
{formatTime(schedule.openTime)} - {formatTime(schedule.closeTime)}
{index < daySchedules.length - 1 && '، '}
</span>
))}
</div>
</div>
))}
</section>
)}
</section>
)
}
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString('fa-IR', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
const secondTab = () => {
const reviews = (reviewsData?.data || []).filter(review => review.isApproved);
const sortedReviews = [...reviews].sort((a, b) => {
if (sorting === '0') {
// جدیدترین
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
} else {
// قدیمی ترین
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
}
});
const averageRating = reviews.length > 0
? reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length
: 0;
const ratingCounts = {
5: reviews.filter(r => r.rating === 5).length,
4: reviews.filter(r => r.rating === 4).length,
3: reviews.filter(r => r.rating === 3).length,
2: reviews.filter(r => r.rating === 2).length,
1: reviews.filter(r => r.rating === 1).length,
};
const totalReviews = reviews.length;
const ratingPercentages = {
5: totalReviews > 0 ? Math.round((ratingCounts[5] / totalReviews) * 100) : 0,
4: totalReviews > 0 ? Math.round((ratingCounts[4] / totalReviews) * 100) : 0,
3: totalReviews > 0 ? Math.round((ratingCounts[3] / totalReviews) * 100) : 0,
2: totalReviews > 0 ? Math.round((ratingCounts[2] / totalReviews) * 100) : 0,
1: totalReviews > 0 ? Math.round((ratingCounts[1] / totalReviews) * 100) : 0,
};
return (
<section aria-labelledby="reviews-title" className='py-4'>
<section
aria-label='امتیاز کاربران'
className="bg-container rounded-container shadow-container p-4 py-6 grid grid-cols-2 items-center">
<div className="text-center font-bold text-5xl">
{averageRating.toFixed(1)}
</div>
<div className="flex flex-col w-fit items-end">
<RateBar className='mt-1' content='5' percentage={String(ratingPercentages[5])} />
<RateBar className='mt-1' content='4' percentage={String(ratingPercentages[4])} />
<RateBar className='mt-1' content='3' percentage={String(ratingPercentages[3])} />
<RateBar className='mt-1' content='2' percentage={String(ratingPercentages[2])} />
<RateBar className='mt-1' content='1' percentage={String(ratingPercentages[1])} />
</div>
</section>
<section className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4">
<div className="flex justify-between items-center border-b-[1.5px] border-border pb-2">
<h2 className='text-sm2 font-medium leading-5'>نظرات کاربران</h2>
<button onClick={toggleSortingModal} className="rounded-xl h-8 bg-container ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]">
<EqualizerIcon className='dark:text-white' />
<span className="text-xs leading-5 font-medium">{sortings[+sorting]}</span>
</button>
</div>
<div className="">
{sortedReviews.length > 0 ? (
sortedReviews.map((review) => (
<article key={review.id}>
<Comment
className='pt-8'
user={review.user?.firstName + ' ' + review.user?.lastName}
rating={review.rating}
date={formatDate(review.createdAt)}
text={review.comment}
tags={[review.food?.title ?? '']}
positivePoints={review.positivePoints}
negativePoints={review.negativePoints}
/>
</article>
))
) : (
<p className='text-sm2 text-center py-8 text-disabled-text'>هنوز نظری ثبت نشده است</p>
)}
</div>
</section>
<AnimatedBottomSheet title="مرتب کردن بر اساس" visible={sortingModal} inDelay={150} onClick={toggleSortingModal}>
<div className="px-8.5 py-10 justify-between">
{sortings.map((v, i) => {
return (
<div key={i}>
<div onClick={() => changeSorting(i)} className="text-sm2 font-normal cursor-pointer">
{v}
</div>
{i < sortings.length - 1 && <hr className="border-white/40 dark:border-border mb-4 mt-4" />}
</div>
)
})}
</div>
</AnimatedBottomSheet>
</section>
)
}
return (
<div className='pt-8 h-full overflow-y-auto noscrollbar'>
<TabContainer>
<TabHeader
viewRenderer={firstTab()}
title='درباره ما' icon={<InfoCircle size={24} />}>
</TabHeader>
<TabHeader
viewRenderer={secondTab()}
title='امتیازات و نظرات' icon={<Star1 size={24} />}>
</TabHeader>
</TabContainer>
</div>
)
}
export default AboutPage