ui ai page
This commit is contained in:
@@ -1,41 +1,32 @@
|
||||
'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 { isReviewApproved } from './types/Types';
|
||||
import AboutSkeleton from './components/AboutSkeleton';
|
||||
import { glassSurfaceCard, glassSurfaceCardFlat, glassSurfaceFlat } from '@/lib/styles/glassSurface';
|
||||
"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 { glassSurfaceCard, glassSurfaceCardFlat, glassSurfaceFlat } from "@/lib/styles/glassSurface";
|
||||
import { ArrowDown2, CallCalling, Clock, Gallery, InfoCircle, Instagram, Location, Star1, Whatsapp } from "iconsax-react";
|
||||
import { useQueryState } from "next-usequerystate";
|
||||
import Image from "next/image";
|
||||
import AboutSkeleton from "./components/AboutSkeleton";
|
||||
import { useGetAbout, useGetReviews, useGetSchedules } from "./hooks/useAboutData";
|
||||
import { isReviewApproved } from "./types/Types";
|
||||
|
||||
const sortings = [
|
||||
'جدیدترین',
|
||||
'قدیمی ترین'
|
||||
]
|
||||
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 [sorting, setSorting] = useQueryState("sortBy", { defaultValue: "0" });
|
||||
const { state: sortingModal, toggle: toggleSortingModal, set: setSortingModal } = useToggle();
|
||||
|
||||
const restaurant = aboutData?.data;
|
||||
const schedules = schedulesData?.data || [];
|
||||
const phoneList = Array.isArray(restaurant?.phones)
|
||||
? restaurant.phones.filter((phone): phone is string => Boolean(phone))
|
||||
: restaurant?.phone
|
||||
? [restaurant.phone]
|
||||
: [];
|
||||
const phoneList = Array.isArray(restaurant?.phones) ? restaurant.phones.filter((phone): phone is string => Boolean(phone)) : restaurant?.phone ? [restaurant.phone] : [];
|
||||
|
||||
const isLoading = aboutLoading || reviewsLoading || schedulesLoading;
|
||||
|
||||
@@ -46,22 +37,22 @@ function AboutPage() {
|
||||
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 days = ["یکشنبه", "دوشنبه", "سهشنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه"];
|
||||
return days[weekDay] || "";
|
||||
};
|
||||
|
||||
const groupSchedulesByDay = () => {
|
||||
const grouped: Record<number, typeof schedules> = {};
|
||||
schedules
|
||||
.filter(schedule => schedule.isActive)
|
||||
.forEach(schedule => {
|
||||
.filter((schedule) => schedule.isActive)
|
||||
.forEach((schedule) => {
|
||||
if (!grouped[schedule.weekDay]) {
|
||||
grouped[schedule.weekDay] = [];
|
||||
}
|
||||
@@ -72,9 +63,7 @@ function AboutPage() {
|
||||
|
||||
const getTodaySchedule = () => {
|
||||
const today = new Date().getDay();
|
||||
const todaySchedules = schedules.filter(schedule =>
|
||||
schedule.isActive && schedule.weekDay === today
|
||||
);
|
||||
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)}`;
|
||||
@@ -86,42 +75,30 @@ function AboutPage() {
|
||||
if (!restaurant) return null;
|
||||
|
||||
return (
|
||||
<section aria-labelledby="about-title" className='py-4'>
|
||||
<section
|
||||
className={glassSurfaceCard('p-4')}>
|
||||
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-[25px]">
|
||||
<section aria-labelledby="about-title" className="py-4">
|
||||
<section className={glassSurfaceCard("p-4")}>
|
||||
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-6.25">
|
||||
<div className="">
|
||||
<h2 className='text-sm2 font-bold leading-5'>{restaurant.name}</h2>
|
||||
{restaurant.establishedYear && (
|
||||
<p className="text-sm2 leading-5 mt-4">تاسیس: {restaurant.establishedYear}</p>
|
||||
)}
|
||||
{restaurant.tagNames && restaurant.tagNames.length > 0 && (
|
||||
<p className="text-sm2 leading-5 mt-2">نوع محصولات: {restaurant.tagNames.join('، ')}</p>
|
||||
)}
|
||||
<h2 className="text-sm2 font-bold leading-5">{restaurant.name}</h2>
|
||||
{restaurant.establishedYear && <p className="text-sm2 leading-5 mt-4">تاسیس: {restaurant.establishedYear}</p>}
|
||||
{restaurant.tagNames && restaurant.tagNames.length > 0 && <p className="text-sm2 leading-5 mt-2">نوع محصولات: {restaurant.tagNames.join("، ")}</p>}
|
||||
</div>
|
||||
{restaurant.logo && (
|
||||
<div className="rounded-normal overflow-clip">
|
||||
<Image
|
||||
alt='logo'
|
||||
src={restaurant.logo}
|
||||
width={88}
|
||||
height={88}
|
||||
unoptimized
|
||||
priority
|
||||
/>
|
||||
<Image alt="logo" src={restaurant.logo} width={88} height={88} unoptimized priority />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{restaurant.description && (
|
||||
<div className="mt-[23px]">
|
||||
<div className="mt-5.75">
|
||||
<h3 className="text-sm2 font-bold leading-5">درباره مجموعه</h3>
|
||||
<p className="text-sm2 leading-5 mt-3 border-spacing-48" style={{ wordSpacing: '0.01em' }}>
|
||||
<p className="text-sm2 leading-5 mt-3 border-spacing-48" style={{ wordSpacing: "0.01em" }}>
|
||||
{restaurant.description}
|
||||
</p>
|
||||
{restaurant.images && restaurant.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 className="inline-flex gap-2 mt-5.75 items-center">
|
||||
<Gallery size={20} className="stroke-disabled-text" />
|
||||
<span className="text-sm2 text-disabled-text font-medium pt-0.5">عکس های رستوران</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -129,60 +106,59 @@ function AboutPage() {
|
||||
</section>
|
||||
|
||||
{(phoneList.length > 0 || restaurant.telegram || restaurant.whatsapp || restaurant.instagram) && (
|
||||
<section
|
||||
className={glassSurfaceCard('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-2 text-center flex justify-center flex-wrap gap-2'>
|
||||
<section className={glassSurfaceCard("pt-3 pb-3.5 px-4 mt-4 grid grid-cols-3 items-center")}>
|
||||
<h2 className="text-sm2 font-medium leading-5">ارتباط</h2>
|
||||
<div className="col-span-2 text-center flex justify-center flex-wrap gap-2">
|
||||
{phoneList.map((phone) => (
|
||||
<a key={phone} href={`tel://${phone}`} className='bg-[#EAEDF5] dark:bg-neutral-700 px-2 py-1.5 rounded-normal inline-flex items-center gap-1.5'>
|
||||
<CallCalling className='stroke-foreground' size={24} />
|
||||
<span dir='ltr' className='text-xs leading-5'>{phone}</span>
|
||||
<a key={phone} href={`tel://${phone}`} className="bg-[#EAEDF5] dark:bg-neutral-700 px-2 py-1.5 rounded-normal inline-flex items-center gap-1.5">
|
||||
<CallCalling className="stroke-foreground" size={24} />
|
||||
<span dir="ltr" className="text-xs leading-5">
|
||||
{phone}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
{restaurant.telegram &&
|
||||
<a href={`https://t.me/${restaurant.telegram}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
|
||||
<TelegramIcon className='stroke-foreground' width={24} height={24} />
|
||||
{restaurant.telegram && (
|
||||
<a href={`https://t.me/${restaurant.telegram}`} className="bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal">
|
||||
<TelegramIcon className="stroke-foreground" width={24} height={24} />
|
||||
</a>
|
||||
}
|
||||
{restaurant.whatsapp &&
|
||||
<a href={`https://whatsapp.com/?phone=${restaurant.whatsapp}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
|
||||
<Whatsapp className='stroke-foreground' size={24} />
|
||||
)}
|
||||
{restaurant.whatsapp && (
|
||||
<a href={`https://whatsapp.com/?phone=${restaurant.whatsapp}`} className="bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal">
|
||||
<Whatsapp className="stroke-foreground" size={24} />
|
||||
</a>
|
||||
}
|
||||
{restaurant.instagram &&
|
||||
<a href={`https://instagram.com/${restaurant.instagram}`} className='bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal'>
|
||||
<Instagram className='stroke-foreground' size={24} />
|
||||
)}
|
||||
{restaurant.instagram && (
|
||||
<a href={`https://instagram.com/${restaurant.instagram}`} className="bg-[#EAEDF5] dark:bg-neutral-700 p-2 rounded-normal">
|
||||
<Instagram className="stroke-foreground" size={24} />
|
||||
</a>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{restaurant.address && (
|
||||
<section className={glassSurfaceCard('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'>{restaurant.address}</p>
|
||||
<section className={glassSurfaceCard("px-4 pt-6 pb-6 mt-4")}>
|
||||
<h2 className="text-sm2 font-medium leading-5">آدرس</h2>
|
||||
<p className="text-sm2 mt-2.25 leading-5">{restaurant.address}</p>
|
||||
{restaurant.latitude && restaurant.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 className="inline-flex gap-2 mt-5.75 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={glassSurfaceCard('py-6 px-4 mt-4 flex justify-between items-center')}>
|
||||
<section aria-label="ساعات کاری" className={glassSurfaceCard("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>
|
||||
<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]' />
|
||||
<ArrowDown2 size={16} className="stroke-[#292D32]" />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
@@ -192,17 +168,13 @@ function AboutPage() {
|
||||
{Object.entries(groupSchedulesByDay())
|
||||
.sort(([a], [b]) => Number(a) - Number(b))
|
||||
.map(([weekDay, daySchedules]) => (
|
||||
<div
|
||||
key={weekDay}
|
||||
className={glassSurfaceCardFlat('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'>
|
||||
<div key={weekDay} className={glassSurfaceCardFlat("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 && '، '}
|
||||
{index < daySchedules.length - 1 && "، "}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -211,15 +183,15 @@ function AboutPage() {
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('fa-IR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
return date.toLocaleDateString("fa-IR", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -227,7 +199,7 @@ function AboutPage() {
|
||||
const reviews = (reviewsData?.data || []).filter(isReviewApproved);
|
||||
|
||||
const sortedReviews = [...reviews].sort((a, b) => {
|
||||
if (sorting === '0') {
|
||||
if (sorting === "0") {
|
||||
// جدیدترین
|
||||
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
||||
} else {
|
||||
@@ -236,16 +208,14 @@ function AboutPage() {
|
||||
}
|
||||
});
|
||||
|
||||
const averageRating = reviews.length > 0
|
||||
? reviews.reduce((sum, review) => sum + review.rating, 0) / reviews.length
|
||||
: 0;
|
||||
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,
|
||||
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;
|
||||
@@ -258,27 +228,23 @@ function AboutPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<section aria-labelledby="reviews-title" className='py-4'>
|
||||
<section
|
||||
aria-label='امتیاز کاربران'
|
||||
className={glassSurfaceCard('p-4 py-6 grid grid-cols-2 items-center')}>
|
||||
<div className="text-center font-bold text-5xl">
|
||||
{averageRating.toFixed(1)}
|
||||
</div>
|
||||
<section aria-labelledby="reviews-title" className="py-4">
|
||||
<section aria-label="امتیاز کاربران" className={glassSurfaceCard("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])} />
|
||||
<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={glassSurfaceCard('pt-3 pb-3.5 px-[16px] mt-4')}>
|
||||
<section className={glassSurfaceCard("pt-3 pb-3.5 px-4 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={glassSurfaceFlat('rounded-xl h-8 pattern-secondary-bg ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]')}>
|
||||
<EqualizerIcon className='dark:text-white' />
|
||||
<h2 className="text-sm2 font-medium leading-5">نظرات کاربران</h2>
|
||||
<button onClick={toggleSortingModal} className={glassSurfaceFlat("rounded-xl h-8 pattern-secondary-bg ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-1.75")}>
|
||||
<EqualizerIcon className="dark:text-white" />
|
||||
<span className="text-xs leading-5 font-medium">{sortings[+sorting]}</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -287,8 +253,8 @@ function AboutPage() {
|
||||
sortedReviews.map((review) => (
|
||||
<article key={review.id}>
|
||||
<Comment
|
||||
className='pt-8'
|
||||
user={review.user?.firstName + ' ' + review.user?.lastName}
|
||||
className="pt-8"
|
||||
user={review.user?.firstName + " " + review.user?.lastName}
|
||||
rating={review.rating}
|
||||
date={formatDate(review.createdAt)}
|
||||
text={review.comment}
|
||||
@@ -299,7 +265,7 @@ function AboutPage() {
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<p className='text-sm2 text-center py-8 text-disabled-text'>هنوز نظری ثبت نشده است</p>
|
||||
<p className="text-sm2 text-center py-8 text-disabled-text">هنوز نظری ثبت نشده است</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
@@ -313,29 +279,22 @@ function AboutPage() {
|
||||
</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'>
|
||||
<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>
|
||||
<TabHeader viewRenderer={firstTab()} title="درباره ما" icon={<InfoCircle size={24} />}></TabHeader>
|
||||
<TabHeader viewRenderer={secondTab()} title="امتیازات و نظرات" icon={<Star1 size={24} />}></TabHeader>
|
||||
</TabContainer>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default AboutPage
|
||||
export default AboutPage;
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import { useGetAbout } from "@/app/[name]/(Main)/about/hooks/useAboutData";
|
||||
import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData";
|
||||
import { glassSurfaceFlat } from "@/lib/styles/glassSurface";
|
||||
import { Send2 } from "iconsax-react";
|
||||
import Image from "next/image";
|
||||
import { FormEvent, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AI_SUGGESTIONS } from "./suggestions";
|
||||
|
||||
type Message = {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
};
|
||||
|
||||
function getGreetingKey() {
|
||||
const hour = new Date().getHours();
|
||||
if (hour >= 5 && hour < 12) return "morning";
|
||||
if (hour >= 12 && hour < 14) return "noon";
|
||||
if (hour >= 14 && hour < 17) return "afternoon";
|
||||
return "evening";
|
||||
}
|
||||
|
||||
function AiPage() {
|
||||
const { t } = useTranslation("common", { keyPrefix: "AI" });
|
||||
const { data: about } = useGetAbout();
|
||||
const { data: profile, isSuccess } = useGetProfile();
|
||||
|
||||
const [input, setInput] = useState("");
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
|
||||
const restaurant = about?.data;
|
||||
const brandName = restaurant?.name || t("BrandFallback");
|
||||
const logoUrl = restaurant?.logo || "/assets/images/danak-logo.png";
|
||||
const userName = isSuccess && profile?.data?.firstName ? profile.data.firstName : t("GuestName");
|
||||
|
||||
const sendMessage = (text: string) => {
|
||||
const content = text.trim();
|
||||
if (!content) return;
|
||||
|
||||
setMessages((prev) => [...prev, { id: `u-${Date.now()}`, role: "user", content }, { id: `a-${Date.now()}`, role: "assistant", content: t("MockReply") }]);
|
||||
setInput("");
|
||||
};
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
sendMessage(input);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="flex h-[calc(100svh-6.5rem)] flex-col pt-4 xl:h-[calc(100svh-6rem)]">
|
||||
<h1 className="shrink-0 text-center text-sm2 font-medium">{t("Heading")}</h1>
|
||||
|
||||
<div className="mt-6 flex min-h-0 flex-1 flex-col">
|
||||
{messages.length > 0 ? (
|
||||
<ul className="noscrollbar flex-1 space-y-3 overflow-y-auto pb-4">
|
||||
{messages.map((message) => (
|
||||
<li
|
||||
key={message.id}
|
||||
className={message.role === "user" ? "rounded-[20px] rounded-tr-none bg-[#F6F7FA] p-4 dark:bg-gray-700" : "rounded-[20px] rounded-tl-none bg-[#EBEDF5] p-4 dark:bg-gray-600"}
|
||||
>
|
||||
<p className="text-xs2 leading-5">{message.content}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Image src={"/assets/images/logo_ai.svg"} alt={brandName} width={299} height={299} className="w-74.75 h-auto" unoptimized />
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col items-center gap-4">
|
||||
<h2 className="text-base font-medium">{t(`Greeting.${getGreetingKey()}`, { name: userName })}</h2>
|
||||
|
||||
<div className="flex w-full flex-wrap gap-2">
|
||||
{AI_SUGGESTIONS.map(({ labelKey, icon: Icon }) => (
|
||||
<button
|
||||
key={labelKey}
|
||||
type="button"
|
||||
onClick={() => sendMessage(t(labelKey))}
|
||||
className={glassSurfaceFlat("inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 transition-[filter] active:brightness-95")}
|
||||
>
|
||||
<Icon size={15} className="stroke-foreground" variant="Bold" />
|
||||
<span className="text-xs2">{t(labelKey)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={onSubmit} className={glassSurfaceFlat("mt-3 flex h-12 shrink-0 items-center gap-1 rounded-xl border border-border px-3")}>
|
||||
<input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder={t("InputPlaceholder")}
|
||||
autoComplete="off"
|
||||
className="h-full w-full bg-transparent text-[11px] outline-none placeholder:text-disabled-text"
|
||||
/>
|
||||
|
||||
<button type="submit" disabled={!input.trim()} className="rounded-full p-1.5 disabled:opacity-40">
|
||||
<Send2 size={20} variant="Bold" className="fill-primary text-primary dark:fill-foreground dark:text-foreground" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default AiPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
'use client'
|
||||
|
||||
export { default } from './AiPage'
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Icon } from 'iconsax-react'
|
||||
import {
|
||||
Apple,
|
||||
Cake,
|
||||
Chart,
|
||||
FavoriteChart,
|
||||
Flash,
|
||||
Health,
|
||||
LikeTag,
|
||||
People,
|
||||
Timer,
|
||||
} from 'iconsax-react'
|
||||
|
||||
export type Suggestion = {
|
||||
labelKey: string
|
||||
icon: Icon
|
||||
}
|
||||
|
||||
export const AI_SUGGESTIONS: Suggestion[] = [
|
||||
{ labelKey: 'Suggestions.vegetarian', icon: Apple },
|
||||
{ labelKey: 'Suggestions.healthy', icon: Health },
|
||||
{ labelKey: 'Suggestions.tasty', icon: LikeTag },
|
||||
{ labelKey: 'Suggestions.protein', icon: FavoriteChart },
|
||||
{ labelKey: 'Suggestions.bestsellers', icon: Chart },
|
||||
{ labelKey: 'Suggestions.spicy', icon: Flash },
|
||||
{ labelKey: 'Suggestions.quick', icon: Timer },
|
||||
{ labelKey: 'Suggestions.dessert', icon: Cake },
|
||||
{ labelKey: 'Suggestions.group', icon: People },
|
||||
]
|
||||
@@ -183,9 +183,9 @@ const MenuIndex = () => {
|
||||
|
||||
<section className="w-full">
|
||||
<div className="flex flex-wrap gap-2 items-center relative" ref={smallCategoriesRef}>
|
||||
<span className="sm:text-base text-sm font-medium max-w-[113px] sm:max-w-auto">{selectedCategory === "0" ? "" : categories.find((c) => c.id === selectedCategory)?.title || ""}</span>
|
||||
<div className="flex min-w-[247px] flex-1 gap-2 justify-end items-center">
|
||||
<button onClick={toggleFilterModal} className={glassSurfaceFlat("rounded-xl h-8 ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]")}>
|
||||
<span className="sm:text-base text-sm font-medium max-w-28.25 sm:max-w-auto">{selectedCategory === "0" ? "" : categories.find((c) => c.id === selectedCategory)?.title || ""}</span>
|
||||
<div className="flex min-w-61.75 flex-1 gap-2 justify-end items-center">
|
||||
<button onClick={toggleFilterModal} className={glassSurfaceFlat("rounded-xl h-8 ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-1.75")}>
|
||||
<Candle2 className="stroke-foreground" size={16} />
|
||||
<span className="text-xs leading-5 font-medium whitespace-nowrap">{tMenu("MenuFilterDrawer.Label")}</span>
|
||||
</button>
|
||||
|
||||
+383
-424
@@ -1,466 +1,425 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useRef, useState, useEffect } from 'react';
|
||||
import { motion, Variants } from 'framer-motion';
|
||||
import SideMenuItem from './SideMenuItem';
|
||||
import NoteBoardIcon from '../icons/NoteBoardIcon';
|
||||
import BlurredOverlayContainer from '../overlays/BlurredOverlayContainer';
|
||||
import NightModeSwitch from '../button/NightModeSwitch';
|
||||
import Button from '../button/PrimaryButton';
|
||||
import { useParams, usePathname, useRouter } from 'next/navigation';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CalendarSearch, Cup, DirectboxReceive, DirectInbox, DocumentCopy, Game, Icon, Instagram, Like1, Login, LogoutCurve, Notification, Receipt1, Setting2, Share, TicketDiscount, Whatsapp } from 'iconsax-react';
|
||||
import TelegramIcon from '../icons/TelegramIcon';
|
||||
import Modal from '../utils/Modal';
|
||||
import LogoutPrompt from '@/features/general/LogoutPrompt';
|
||||
import useToggle from '@/hooks/helpers/useToggle';
|
||||
import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
|
||||
import Image from 'next/image';
|
||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||
import { toast, toastLoginRequired } from '../Toast';
|
||||
import { glassSurface } from '@/lib/styles/glassSurface';
|
||||
import { useGetAbout } from "@/app/[name]/(Main)/about/hooks/useAboutData";
|
||||
import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData";
|
||||
import LogoutPrompt from "@/features/general/LogoutPrompt";
|
||||
import useToggle from "@/hooks/helpers/useToggle";
|
||||
import { glassSurface } from "@/lib/styles/glassSurface";
|
||||
import clsx from "clsx";
|
||||
import { motion, Variants } from "framer-motion";
|
||||
import {
|
||||
CalendarSearch,
|
||||
Cup,
|
||||
DirectboxReceive,
|
||||
DirectInbox,
|
||||
DocumentCopy,
|
||||
Game,
|
||||
Icon,
|
||||
Instagram,
|
||||
Like1,
|
||||
Login,
|
||||
LogoutCurve,
|
||||
MessageProgramming,
|
||||
Notification,
|
||||
Receipt1,
|
||||
Setting2,
|
||||
Share,
|
||||
TicketDiscount,
|
||||
Whatsapp,
|
||||
} from "iconsax-react";
|
||||
import Image from "next/image";
|
||||
import { useParams, usePathname, useRouter } from "next/navigation";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import NightModeSwitch from "../button/NightModeSwitch";
|
||||
import Button from "../button/PrimaryButton";
|
||||
import NoteBoardIcon from "../icons/NoteBoardIcon";
|
||||
import TelegramIcon from "../icons/TelegramIcon";
|
||||
import BlurredOverlayContainer from "../overlays/BlurredOverlayContainer";
|
||||
import { toast, toastLoginRequired } from "../Toast";
|
||||
import Modal from "../utils/Modal";
|
||||
import SideMenuItem from "./SideMenuItem";
|
||||
|
||||
type MenuItemType = {
|
||||
href: string | undefined;
|
||||
title: string;
|
||||
icon: Icon;
|
||||
auth?: boolean;
|
||||
guestOnly?: boolean;
|
||||
premiumOnly?: boolean;
|
||||
href: string | undefined;
|
||||
title: string;
|
||||
icon: Icon;
|
||||
auth?: boolean;
|
||||
guestOnly?: boolean;
|
||||
premiumOnly?: boolean;
|
||||
};
|
||||
|
||||
const menuItems: Array<Array<MenuItemType>> = [
|
||||
[
|
||||
{ href: 'notifications', title: 'Notifications', icon: Notification },
|
||||
{ href: 'transactions/discount/club', title: 'CustomerClub', icon: Cup, premiumOnly: true },
|
||||
{ href: 'transactions/discount', title: 'Discounts', icon: TicketDiscount, premiumOnly: true },
|
||||
{ href: 'order/history', title: 'Orders', icon: CalendarSearch, premiumOnly: true },
|
||||
{ href: 'transactions', title: 'Transactions', icon: Receipt1, premiumOnly: true },
|
||||
{ href: 'game', title: 'Games', icon: Game },
|
||||
{ href: '?share', title: 'ShareWithFriends', icon: Like1 },
|
||||
{ href: 'report', title: 'ReportProblem', icon: NoteBoardIcon },
|
||||
{ href: '?installpwa', title: 'InstallApp', icon: DirectboxReceive }
|
||||
],
|
||||
[],
|
||||
[
|
||||
{ auth: true, href: 'profile/settings', title: 'Preferences', icon: Setting2 },
|
||||
{ auth: true, href: '?logout', title: 'Logout', icon: LogoutCurve },
|
||||
{ guestOnly: true, href: 'auth', title: 'LoginSignup', icon: Login }
|
||||
]
|
||||
[
|
||||
{ href: "notifications", title: "Notifications", icon: Notification },
|
||||
{ href: "transactions/discount/club", title: "CustomerClub", icon: Cup, premiumOnly: true },
|
||||
{ href: "transactions/discount", title: "Discounts", icon: TicketDiscount, premiumOnly: true },
|
||||
{ href: "order/history", title: "Orders", icon: CalendarSearch, premiumOnly: true },
|
||||
{ href: "transactions", title: "Transactions", icon: Receipt1, premiumOnly: true },
|
||||
{ href: "game", title: "Games", icon: Game },
|
||||
{ href: "ai", title: "AI", icon: MessageProgramming },
|
||||
{ href: "?share", title: "ShareWithFriends", icon: Like1 },
|
||||
{ href: "report", title: "ReportProblem", icon: NoteBoardIcon },
|
||||
{ href: "?installpwa", title: "InstallApp", icon: DirectboxReceive },
|
||||
],
|
||||
[],
|
||||
[
|
||||
{ auth: true, href: "profile/settings", title: "Preferences", icon: Setting2 },
|
||||
{ auth: true, href: "?logout", title: "Logout", icon: LogoutCurve },
|
||||
{ guestOnly: true, href: "auth", title: "LoginSignup", icon: Login },
|
||||
],
|
||||
];
|
||||
|
||||
type Props = {
|
||||
menuState: boolean;
|
||||
toggleMenuState: React.MouseEventHandler<HTMLDivElement> | undefined;
|
||||
nightModeState: boolean;
|
||||
togglenightModeState: React.MouseEventHandler<HTMLDivElement> | undefined;
|
||||
menuState: boolean;
|
||||
toggleMenuState: React.MouseEventHandler<HTMLDivElement> | undefined;
|
||||
nightModeState: boolean;
|
||||
togglenightModeState: React.MouseEventHandler<HTMLDivElement> | undefined;
|
||||
};
|
||||
|
||||
type BeforeInstallPromptEvent = Event & {
|
||||
prompt: () => Promise<void>;
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
|
||||
prompt: () => Promise<void>;
|
||||
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
|
||||
};
|
||||
|
||||
const isIOS = () => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||
if (typeof window === "undefined") return false;
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
||||
};
|
||||
|
||||
const isStandalone = () => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
if (typeof window === "undefined") return false;
|
||||
|
||||
const isIOSStandalone =
|
||||
('standalone' in window.navigator) &&
|
||||
(window.navigator as { standalone?: boolean }).standalone === true;
|
||||
const isIOSStandalone = "standalone" in window.navigator && (window.navigator as { standalone?: boolean }).standalone === true;
|
||||
|
||||
const isDisplayModeStandalone =
|
||||
window.matchMedia('(display-mode: standalone)').matches ||
|
||||
window.matchMedia('(display-mode: fullscreen)').matches;
|
||||
const isDisplayModeStandalone = window.matchMedia("(display-mode: standalone)").matches || window.matchMedia("(display-mode: fullscreen)").matches;
|
||||
|
||||
return isIOSStandalone || isDisplayModeStandalone;
|
||||
return isIOSStandalone || isDisplayModeStandalone;
|
||||
};
|
||||
|
||||
function SideMenu({ menuState, toggleMenuState, nightModeState, togglenightModeState }: Props) {
|
||||
const menuStateMemo = useMemo(() => menuState, [menuState]);
|
||||
const closeRef = useRef<HTMLDivElement>(null);
|
||||
const { state: logoutModal, toggle: toggleLogoutModal } = useToggle();
|
||||
const { state: shareModal, toggle: toggleShareModal } = useToggle();
|
||||
const { state: installPwaModal, toggle: toggleInstallPwaModal } = useToggle();
|
||||
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
||||
const [isIOSDevice, setIsIOSDevice] = useState(false);
|
||||
const [isPWAInstalled, setIsPWAInstalled] = useState(false);
|
||||
const { data: aboutData } = useGetAbout();
|
||||
const { data: profile, isSuccess } = useGetProfile();
|
||||
const profileData = profile?.data;
|
||||
const isLoggedIn = isSuccess && profileData;
|
||||
const isPremium = aboutData?.data?.plan === "premium";
|
||||
const router = useRouter();
|
||||
|
||||
const menuStateMemo = useMemo(() => menuState, [menuState]);
|
||||
const closeRef = useRef<HTMLDivElement>(null);
|
||||
const { state: logoutModal, toggle: toggleLogoutModal } = useToggle();
|
||||
const { state: shareModal, toggle: toggleShareModal } = useToggle();
|
||||
const { state: installPwaModal, toggle: toggleInstallPwaModal } = useToggle();
|
||||
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
||||
const [isIOSDevice, setIsIOSDevice] = useState(false);
|
||||
const [isPWAInstalled, setIsPWAInstalled] = useState(false);
|
||||
const { data: aboutData } = useGetAbout();
|
||||
const { data: profile, isSuccess } = useGetProfile();
|
||||
const profileData = profile?.data;
|
||||
const isLoggedIn = isSuccess && profileData;
|
||||
const isPremium = aboutData?.data?.plan === 'premium';
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const { name } = params;
|
||||
const pathname = usePathname();
|
||||
const { t: tMenu } = useTranslation("common", {
|
||||
keyPrefix: "SideMenu",
|
||||
});
|
||||
const { t: tShareModal } = useTranslation("common", {
|
||||
keyPrefix: "ShareModal",
|
||||
});
|
||||
const { t: tInstallPwaModal } = useTranslation("common", {
|
||||
keyPrefix: "InstallPwaModal",
|
||||
});
|
||||
|
||||
const variants: Variants = {
|
||||
hidden: { x: "100%", transition: { duration: 0.1, ease: "easeInOut" } },
|
||||
visible: { x: 0, transition: { delay: 0.075, duration: 0.1, ease: "easeInOut" } },
|
||||
};
|
||||
|
||||
const params = useParams();
|
||||
const { name } = params;
|
||||
const pathname = usePathname();
|
||||
const { t: tMenu } = useTranslation('common', {
|
||||
keyPrefix: 'SideMenu'
|
||||
});
|
||||
const { t: tShareModal } = useTranslation('common', {
|
||||
keyPrefix: 'ShareModal'
|
||||
});
|
||||
const { t: tInstallPwaModal } = useTranslation('common', {
|
||||
keyPrefix: 'InstallPwaModal'
|
||||
});
|
||||
useEffect(() => {
|
||||
const ios = isIOS();
|
||||
const standalone = isStandalone();
|
||||
setIsIOSDevice(ios);
|
||||
setIsPWAInstalled(standalone);
|
||||
|
||||
const variants: Variants = {
|
||||
hidden: { x: '100%', transition: { duration: 0.1, ease: 'easeInOut' } },
|
||||
visible: { x: 0, transition: { delay: 0.075, duration: 0.1, ease: 'easeInOut' } }
|
||||
const handleBeforeInstallPrompt = (e: Event) => {
|
||||
e.preventDefault();
|
||||
setDeferredPrompt(e as BeforeInstallPromptEvent);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const ios = isIOS();
|
||||
const standalone = isStandalone();
|
||||
setIsIOSDevice(ios);
|
||||
setIsPWAInstalled(standalone);
|
||||
|
||||
const handleBeforeInstallPrompt = (e: Event) => {
|
||||
e.preventDefault();
|
||||
setDeferredPrompt(e as BeforeInstallPromptEvent);
|
||||
};
|
||||
|
||||
const handleAppInstalled = () => {
|
||||
setDeferredPrompt(null);
|
||||
setIsPWAInstalled(true);
|
||||
};
|
||||
|
||||
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||
window.addEventListener('appinstalled', handleAppInstalled);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
||||
window.removeEventListener('appinstalled', handleAppInstalled);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleInstallPWA = async () => {
|
||||
if (!deferredPrompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
|
||||
if (outcome === 'accepted') {
|
||||
setDeferredPrompt(null);
|
||||
toggleInstallPwaModal();
|
||||
}
|
||||
const handleAppInstalled = () => {
|
||||
setDeferredPrompt(null);
|
||||
setIsPWAInstalled(true);
|
||||
};
|
||||
|
||||
const getShareUrl = () => {
|
||||
if (typeof window === 'undefined') return '';
|
||||
return window.location.href;
|
||||
window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
|
||||
window.addEventListener("appinstalled", handleAppInstalled);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
|
||||
window.removeEventListener("appinstalled", handleAppInstalled);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const copyToClipboard = async (showToast: boolean = true) => {
|
||||
try {
|
||||
const url = getShareUrl();
|
||||
await navigator.clipboard.writeText(url);
|
||||
if (showToast) {
|
||||
toast('لینک با موفقیت کپی شد', 'success');
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
if (showToast) {
|
||||
toast('خطا در کپی کردن لینک', 'error');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
await copyToClipboard();
|
||||
toggleShareModal();
|
||||
};
|
||||
|
||||
const handleShareTelegram = () => {
|
||||
const url = encodeURIComponent(getShareUrl());
|
||||
window.open(`https://t.me/share/url?url=${url}`, '_blank');
|
||||
};
|
||||
|
||||
const handleShareWhatsApp = () => {
|
||||
const url = encodeURIComponent(getShareUrl());
|
||||
const text = encodeURIComponent('اپلیکیشن ما را ببینید!');
|
||||
window.open(`https://wa.me/?text=${text}%20${url}`, '_blank');
|
||||
};
|
||||
|
||||
const handleShareInstagram = async () => {
|
||||
const success = await copyToClipboard(false);
|
||||
if (success) {
|
||||
toast('لینک کپی شد. لطفاً آن را در اینستاگرام به اشتراک بگذارید', 'info');
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotificationClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (!isLoggedIn) {
|
||||
toastLoginRequired(() => router.push(`/${name}/auth`), 'info');
|
||||
} else {
|
||||
router.push(`/${name}/notifications`);
|
||||
}
|
||||
};
|
||||
|
||||
const hrefOnClicks = [
|
||||
{ href: '?logout', handler: toggleLogoutModal },
|
||||
{ href: '?share', handler: toggleShareModal },
|
||||
{ href: '?installpwa', handler: toggleInstallPwaModal },
|
||||
{ href: 'notifications', handler: handleNotificationClick }
|
||||
];
|
||||
|
||||
const renderMenu = () => {
|
||||
return (
|
||||
<section className="flex-1 overflow-y-scroll noscrollbar pt-6 pb-8 flex flex-col justify-between md:w-[200px] xl:w-[250px]">
|
||||
{
|
||||
aboutData?.data?.logo && (
|
||||
<div >
|
||||
<Image className='mx-auto' src={aboutData?.data?.logo} alt='logo' width={110} height={100} unoptimized />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='mt-4'>
|
||||
<header className="px-5 md:px-6 xl:px-12">
|
||||
<h6 className="text-start font-bold text-sm text-menu-header mt-2 mb-[19px] dark:text-disabled-text">منو</h6>
|
||||
</header>
|
||||
|
||||
<nav aria-label={tMenu('NavAriaLabel')}>
|
||||
<ul>
|
||||
{menuItems[0]
|
||||
.filter(item => {
|
||||
if (item.auth && !isSuccess) return false;
|
||||
if (item.guestOnly && isSuccess) return false;
|
||||
if (item.href === '?installpwa' && isPWAInstalled) return false;
|
||||
if (item.premiumOnly && !isPremium) return false;
|
||||
return true;
|
||||
})
|
||||
.map(({ icon: Icon, ...item }, index) => {
|
||||
const href = `/${name}/${item.href}`;
|
||||
const isActive = pathname === href;
|
||||
return (
|
||||
<li key={index} className="mt-4 h-6">
|
||||
<SideMenuItem
|
||||
className={clsx(isActive && 'text-primary! border-primary! dark:text-neutral-200! dark:border-neutral-200!', 'border-r-4!')}
|
||||
href={href ?? ''}
|
||||
title={tMenu(item.title)}
|
||||
onClick={
|
||||
typeof item.href === 'string'
|
||||
? hrefOnClicks.find((i) => i.href === item.href)?.handler
|
||||
: undefined
|
||||
}
|
||||
icon={
|
||||
<Icon
|
||||
variant={isActive ? 'Bold' : 'Linear'}
|
||||
className={clsx(
|
||||
isActive ? 'text-primary fill-primary dark:text-neutral-200 dark:fill-neutral-200' : 'stroke-icon-deactive text-icon-deactive'
|
||||
)}
|
||||
size={20} width={20} height={20} />
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<section aria-label={tMenu('ControlsAriaLabel')}>
|
||||
<ul className="flex flex-col pt-16">
|
||||
{menuItems[2]
|
||||
.filter(item => {
|
||||
if (item.auth && !isSuccess) return false;
|
||||
if (item.guestOnly && isSuccess) return false;
|
||||
return true;
|
||||
})
|
||||
.map(({ icon: Icon, ...item }, index) => {
|
||||
const href = `/${name}/${item.href}`;
|
||||
const isActive = pathname === href;
|
||||
return (
|
||||
<li key={index} className="mt-4 h-6 w-full">
|
||||
<SideMenuItem
|
||||
key={index}
|
||||
href={href ?? ''}
|
||||
className={clsx(isActive && 'text-primary! border-primary! dark:text-neutral-200! dark:border-neutral-200!', 'border-r-4!')}
|
||||
onClick={
|
||||
typeof item.href === 'string'
|
||||
? hrefOnClicks.find((i) => i.href === item.href)?.handler
|
||||
: undefined
|
||||
}
|
||||
title={tMenu(item.title)}
|
||||
icon={
|
||||
<Icon
|
||||
variant={isActive ? 'Bold' : 'Linear'}
|
||||
className={clsx(
|
||||
isActive ? 'text-primary fill-primary dark:text-neutral-200 dark:fill-neutral-200' : 'stroke-icon-deactive text-icon-deactive'
|
||||
)}
|
||||
size={20} width={20} height={20} />
|
||||
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
<li className="mt-4 h-6">
|
||||
<SideMenuItem
|
||||
className='pr-7'
|
||||
href={''}
|
||||
title={''}
|
||||
icon={<></>}>
|
||||
<NightModeSwitch
|
||||
checked={nightModeState}
|
||||
onClick={togglenightModeState}
|
||||
/>
|
||||
</SideMenuItem>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
)
|
||||
const handleInstallPWA = async () => {
|
||||
if (!deferredPrompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
|
||||
if (outcome === "accepted") {
|
||||
setDeferredPrompt(null);
|
||||
toggleInstallPwaModal();
|
||||
}
|
||||
};
|
||||
|
||||
const getShareUrl = () => {
|
||||
if (typeof window === "undefined") return "";
|
||||
return window.location.href;
|
||||
};
|
||||
|
||||
const copyToClipboard = async (showToast: boolean = true) => {
|
||||
try {
|
||||
const url = getShareUrl();
|
||||
await navigator.clipboard.writeText(url);
|
||||
if (showToast) {
|
||||
toast("لینک با موفقیت کپی شد", "success");
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
if (showToast) {
|
||||
toast("خطا در کپی کردن لینک", "error");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
await copyToClipboard();
|
||||
toggleShareModal();
|
||||
};
|
||||
|
||||
const handleShareTelegram = () => {
|
||||
const url = encodeURIComponent(getShareUrl());
|
||||
window.open(`https://t.me/share/url?url=${url}`, "_blank");
|
||||
};
|
||||
|
||||
const handleShareWhatsApp = () => {
|
||||
const url = encodeURIComponent(getShareUrl());
|
||||
const text = encodeURIComponent("اپلیکیشن ما را ببینید!");
|
||||
window.open(`https://wa.me/?text=${text}%20${url}`, "_blank");
|
||||
};
|
||||
|
||||
const handleShareInstagram = async () => {
|
||||
const success = await copyToClipboard(false);
|
||||
if (success) {
|
||||
toast("لینک کپی شد. لطفاً آن را در اینستاگرام به اشتراک بگذارید", "info");
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotificationClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (!isLoggedIn) {
|
||||
toastLoginRequired(() => router.push(`/${name}/auth`), "info");
|
||||
} else {
|
||||
router.push(`/${name}/notifications`);
|
||||
}
|
||||
};
|
||||
|
||||
const hrefOnClicks = [
|
||||
{ href: "?logout", handler: toggleLogoutModal },
|
||||
{ href: "?share", handler: toggleShareModal },
|
||||
{ href: "?installpwa", handler: toggleInstallPwaModal },
|
||||
{ href: "notifications", handler: handleNotificationClick },
|
||||
];
|
||||
|
||||
const renderMenu = () => {
|
||||
return (
|
||||
<>
|
||||
<div className='w-full' ref={closeRef} onClick={toggleMenuState}>
|
||||
<aside aria-label={tMenu('AriaLabel')}>
|
||||
<BlurredOverlayContainer className='xl:hidden!' bgOpacity={40} visible={menuStateMemo} outDelay={150} inDuration={200}>
|
||||
<motion.nav
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
initial="hidden"
|
||||
animate={menuState ? 'visible' : 'hidden'}
|
||||
variants={variants}
|
||||
data-visible={menuState}
|
||||
data-isvisible={menuStateMemo}
|
||||
onClick={(e) => { e.stopPropagation(); }}
|
||||
className="fixed top-0 bottom-0 right-0 bg-container w-[200px] h-dvh flex flex-col z-40 overflow-clip not-xl:rounded-s-none!"
|
||||
>
|
||||
{renderMenu()}
|
||||
</motion.nav>
|
||||
</BlurredOverlayContainer>
|
||||
<nav
|
||||
className={glassSurface('hidden fixed top-4 bottom-4 right-4 xl:w-[250px] xl:flex flex-col z-40 overflow-clip xl:rounded-4xl')}
|
||||
>
|
||||
{renderMenu()}
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
<section className="flex-1 overflow-y-scroll noscrollbar pt-6 pb-8 flex flex-col justify-between md:w-50 xl:w-62.5">
|
||||
{aboutData?.data?.logo && (
|
||||
<div>
|
||||
<Image className="mx-auto" src={aboutData?.data?.logo} alt="logo" width={110} height={100} unoptimized />
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4">
|
||||
<header className="px-5 md:px-6 xl:px-12">
|
||||
<h6 className="text-start font-bold text-sm text-menu-header mt-2 mb-4.75 dark:text-disabled-text">منو</h6>
|
||||
</header>
|
||||
|
||||
{/* Logout Modal */}
|
||||
<LogoutPrompt
|
||||
visible={logoutModal}
|
||||
onClick={toggleLogoutModal}
|
||||
slug={name as string}
|
||||
/>
|
||||
<nav aria-label={tMenu("NavAriaLabel")}>
|
||||
<ul>
|
||||
{menuItems[0]
|
||||
.filter((item) => {
|
||||
if (item.auth && !isSuccess) return false;
|
||||
if (item.guestOnly && isSuccess) return false;
|
||||
if (item.href === "?installpwa" && isPWAInstalled) return false;
|
||||
if (item.premiumOnly && !isPremium) return false;
|
||||
return true;
|
||||
})
|
||||
.map(({ icon: Icon, ...item }, index) => {
|
||||
const href = `/${name}/${item.href}`;
|
||||
const isActive = pathname === href;
|
||||
return (
|
||||
<li key={index} className="mt-4 h-6">
|
||||
<SideMenuItem
|
||||
className={clsx(isActive && "text-primary! border-primary! dark:text-neutral-200! dark:border-neutral-200!", "border-r-4!")}
|
||||
href={href ?? ""}
|
||||
title={tMenu(item.title)}
|
||||
onClick={typeof item.href === "string" ? hrefOnClicks.find((i) => i.href === item.href)?.handler : undefined}
|
||||
icon={
|
||||
<Icon
|
||||
variant={isActive ? "Bold" : "Linear"}
|
||||
className={clsx(isActive ? "text-primary fill-primary dark:text-neutral-200 dark:fill-neutral-200" : "stroke-icon-deactive text-icon-deactive")}
|
||||
size={20}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Share modal */}
|
||||
<Modal
|
||||
visible={shareModal}
|
||||
onClick={toggleShareModal}
|
||||
>
|
||||
<h2 className="text-base font-medium">
|
||||
<Share className="inline-block ml-2 mb-1.5 stroke-primary dark:stroke-foreground" size={24} />
|
||||
{tShareModal('Heading')}
|
||||
</h2>
|
||||
<p className="text-xs font-medium mt-6">
|
||||
{tShareModal('Description')}
|
||||
</p>
|
||||
|
||||
<Button onClick={handleCopyLink} className="text-sm mt-8">
|
||||
<DocumentCopy className="inline-block ml-2 mb-0.5 stroke-container dark:stroke-foreground" size={20} />
|
||||
<span className="font-light">{tShareModal('ButtonCopy')}</span>
|
||||
</Button>
|
||||
|
||||
<div className="grid grid-cols-3 gap-5.5 mt-6 px-12">
|
||||
<button
|
||||
onClick={handleShareTelegram}
|
||||
className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<TelegramIcon width={24} height={24} className="mb-1.5 text-primary dark:text-foreground" />
|
||||
<span className='text-xs2 font-medium'>Telegram</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleShareWhatsApp}
|
||||
className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<Whatsapp size={24} className="mb-1.5 stroke-primary dark:stroke-foreground" />
|
||||
<span className='text-xs2 font-medium'>WhatsApp</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleShareInstagram}
|
||||
className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<Instagram size={24} className="mb-1.5 stroke-primary dark:stroke-foreground" />
|
||||
<span className='text-xs2 font-medium'>Instagram</span>
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Install PWA modal */}
|
||||
<Modal
|
||||
visible={installPwaModal}
|
||||
onClick={toggleInstallPwaModal}
|
||||
>
|
||||
<h2 className="text-base font-medium">
|
||||
<DirectInbox className="inline-block ml-2 mb-1.5 stroke-primary dark:stroke-foreground" size={24} />
|
||||
{tInstallPwaModal('Heading')}
|
||||
</h2>
|
||||
{isPWAInstalled ? (
|
||||
<p className="mt-6 text-xs font-medium text-center text-foreground/80 dark:text-neutral-200">
|
||||
{tInstallPwaModal('AlreadyInstalled')}
|
||||
</p>
|
||||
) : isIOSDevice ? (
|
||||
<div className="mt-5 rounded-3xl bg-primary/5 px-4 py-5 backdrop-blur-sm dark:bg-white/5">
|
||||
<p className="text-xs font-medium leading-relaxed text-center text-foreground/80 dark:text-neutral-200">
|
||||
{tInstallPwaModal('IOSDescription')}
|
||||
</p>
|
||||
<ol className="mt-5 space-y-3 text-xs font-medium rtl:text-right">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">
|
||||
۱
|
||||
</span>
|
||||
<span>{tInstallPwaModal('IOSStep1')}</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">
|
||||
۲
|
||||
</span>
|
||||
<span>{tInstallPwaModal('IOSStep2')}</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">
|
||||
۳
|
||||
</span>
|
||||
<span>{tInstallPwaModal('IOSStep3')}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="mt-6 text-xs font-medium leading-relaxed text-center text-foreground/80 dark:text-neutral-200">
|
||||
{tInstallPwaModal('Description')}
|
||||
</p>
|
||||
{deferredPrompt ? (
|
||||
<Button onClick={handleInstallPWA} className="mt-8 text-sm">
|
||||
<span className="font-light">{tInstallPwaModal('ButtonOk')}</span>
|
||||
</Button>
|
||||
) : (
|
||||
<p className="mt-4 text-[11px] font-medium leading-relaxed text-center text-foreground/60 dark:text-neutral-300">
|
||||
{tInstallPwaModal('ChromeFallback')}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
</Modal>
|
||||
</>
|
||||
<section aria-label={tMenu("ControlsAriaLabel")}>
|
||||
<ul className="flex flex-col pt-16">
|
||||
{menuItems[2]
|
||||
.filter((item) => {
|
||||
if (item.auth && !isSuccess) return false;
|
||||
if (item.guestOnly && isSuccess) return false;
|
||||
return true;
|
||||
})
|
||||
.map(({ icon: Icon, ...item }, index) => {
|
||||
const href = `/${name}/${item.href}`;
|
||||
const isActive = pathname === href;
|
||||
return (
|
||||
<li key={index} className="mt-4 h-6 w-full">
|
||||
<SideMenuItem
|
||||
key={index}
|
||||
href={href ?? ""}
|
||||
className={clsx(isActive && "text-primary! border-primary! dark:text-neutral-200! dark:border-neutral-200!", "border-r-4!")}
|
||||
onClick={typeof item.href === "string" ? hrefOnClicks.find((i) => i.href === item.href)?.handler : undefined}
|
||||
title={tMenu(item.title)}
|
||||
icon={
|
||||
<Icon
|
||||
variant={isActive ? "Bold" : "Linear"}
|
||||
className={clsx(isActive ? "text-primary fill-primary dark:text-neutral-200 dark:fill-neutral-200" : "stroke-icon-deactive text-icon-deactive")}
|
||||
size={20}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
<li className="mt-4 h-6">
|
||||
<SideMenuItem className="pr-7" href={""} title={""} icon={<></>}>
|
||||
<NightModeSwitch checked={nightModeState} onClick={togglenightModeState} />
|
||||
</SideMenuItem>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full" ref={closeRef} onClick={toggleMenuState}>
|
||||
<aside aria-label={tMenu("AriaLabel")}>
|
||||
<BlurredOverlayContainer className="xl:hidden!" bgOpacity={40} visible={menuStateMemo} outDelay={150} inDuration={200}>
|
||||
<motion.nav
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
initial="hidden"
|
||||
animate={menuState ? "visible" : "hidden"}
|
||||
variants={variants}
|
||||
data-visible={menuState}
|
||||
data-isvisible={menuStateMemo}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="fixed top-0 bottom-0 right-0 bg-container w-50 h-dvh flex flex-col z-40 overflow-clip not-xl:rounded-s-none!"
|
||||
>
|
||||
{renderMenu()}
|
||||
</motion.nav>
|
||||
</BlurredOverlayContainer>
|
||||
<nav className={glassSurface("hidden fixed top-4 bottom-4 right-4 xl:w-62.5 xl:flex flex-col z-40 overflow-clip xl:rounded-4xl")}>{renderMenu()}</nav>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* Logout Modal */}
|
||||
<LogoutPrompt visible={logoutModal} onClick={toggleLogoutModal} slug={name as string} />
|
||||
|
||||
{/* Share modal */}
|
||||
<Modal visible={shareModal} onClick={toggleShareModal}>
|
||||
<h2 className="text-base font-medium">
|
||||
<Share className="inline-block ml-2 mb-1.5 stroke-primary dark:stroke-foreground" size={24} />
|
||||
{tShareModal("Heading")}
|
||||
</h2>
|
||||
<p className="text-xs font-medium mt-6">{tShareModal("Description")}</p>
|
||||
|
||||
<Button onClick={handleCopyLink} className="text-sm mt-8">
|
||||
<DocumentCopy className="inline-block ml-2 mb-0.5 stroke-container dark:stroke-foreground" size={20} />
|
||||
<span className="font-light">{tShareModal("ButtonCopy")}</span>
|
||||
</Button>
|
||||
|
||||
<div className="grid grid-cols-3 gap-5.5 mt-6 px-12">
|
||||
<button onClick={handleShareTelegram} className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity">
|
||||
<TelegramIcon width={24} height={24} className="mb-1.5 text-primary dark:text-foreground" />
|
||||
<span className="text-xs2 font-medium">Telegram</span>
|
||||
</button>
|
||||
<button onClick={handleShareWhatsApp} className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity">
|
||||
<Whatsapp size={24} className="mb-1.5 stroke-primary dark:stroke-foreground" />
|
||||
<span className="text-xs2 font-medium">WhatsApp</span>
|
||||
</button>
|
||||
<button onClick={handleShareInstagram} className="flex flex-col items-center cursor-pointer hover:opacity-80 transition-opacity">
|
||||
<Instagram size={24} className="mb-1.5 stroke-primary dark:stroke-foreground" />
|
||||
<span className="text-xs2 font-medium">Instagram</span>
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Install PWA modal */}
|
||||
<Modal visible={installPwaModal} onClick={toggleInstallPwaModal}>
|
||||
<h2 className="text-base font-medium">
|
||||
<DirectInbox className="inline-block ml-2 mb-1.5 stroke-primary dark:stroke-foreground" size={24} />
|
||||
{tInstallPwaModal("Heading")}
|
||||
</h2>
|
||||
{isPWAInstalled ? (
|
||||
<p className="mt-6 text-xs font-medium text-center text-foreground/80 dark:text-neutral-200">{tInstallPwaModal("AlreadyInstalled")}</p>
|
||||
) : isIOSDevice ? (
|
||||
<div className="mt-5 rounded-3xl bg-primary/5 px-4 py-5 backdrop-blur-sm dark:bg-white/5">
|
||||
<p className="text-xs font-medium leading-relaxed text-center text-foreground/80 dark:text-neutral-200">{tInstallPwaModal("IOSDescription")}</p>
|
||||
<ol className="mt-5 space-y-3 text-xs font-medium rtl:text-right">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">۱</span>
|
||||
<span>{tInstallPwaModal("IOSStep1")}</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">۲</span>
|
||||
<span>{tInstallPwaModal("IOSStep2")}</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary/10 text-[11px] font-bold text-primary">۳</span>
|
||||
<span>{tInstallPwaModal("IOSStep3")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="mt-6 text-xs font-medium leading-relaxed text-center text-foreground/80 dark:text-neutral-200">{tInstallPwaModal("Description")}</p>
|
||||
{deferredPrompt ? (
|
||||
<Button onClick={handleInstallPWA} className="mt-8 text-sm">
|
||||
<span className="font-light">{tInstallPwaModal("ButtonOk")}</span>
|
||||
</Button>
|
||||
) : (
|
||||
<p className="mt-4 text-[11px] font-medium leading-relaxed text-center text-foreground/60 dark:text-neutral-300">{tInstallPwaModal("ChromeFallback")}</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SideMenu;
|
||||
export default SideMenu;
|
||||
|
||||
@@ -43,7 +43,7 @@ function ClientMenuRouteWrapper({ children }: Props) {
|
||||
};
|
||||
|
||||
const location = usePathname();
|
||||
const hideBottomNav = pathname.endsWith("/cart");
|
||||
const hideBottomNav = pathname.endsWith("/cart") || pathname.endsWith("/ai");
|
||||
const mainRef = useRef<HTMLElement>(null);
|
||||
|
||||
// Refs for the scroll-driven animation — kept outside React state so scroll
|
||||
@@ -75,8 +75,8 @@ function ClientMenuRouteWrapper({ children }: Props) {
|
||||
}
|
||||
|
||||
const SCROLL_RANGE = 150; // px of scroll to reach full compact state
|
||||
const LERP_FACTOR = 0.1; // per-frame smoothing (lower = smoother, higher = snappier)
|
||||
const MIN_SCALE = 0.82; // scale floor (18% smaller at full scroll)
|
||||
const LERP_FACTOR = 0.1; // per-frame smoothing (lower = smoother, higher = snappier)
|
||||
const MIN_SCALE = 0.82; // scale floor (18% smaller at full scroll)
|
||||
|
||||
const tick = () => {
|
||||
const diff = targetProgressRef.current - currentProgressRef.current;
|
||||
@@ -106,7 +106,7 @@ function ClientMenuRouteWrapper({ children }: Props) {
|
||||
|
||||
return (
|
||||
<div className="h-svh overflow-hidden pt-10 flex flex-col pb-4 xl:pt-12">
|
||||
<div data-menu-topbar className={glassSurface("z-50 fixed right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-4 justify-between rounded-[32px] xl:w-[calc(100%-305px)]")}>
|
||||
<div data-menu-topbar className={glassSurface("z-50 fixed right-4 left-4 xl:right-71.25 top-4 xl:h-16 h-12 flex items-center px-4 justify-between rounded-4xl xl:w-[calc(100%-305px)]")}>
|
||||
<TopBar
|
||||
profileDropState={profileDrop}
|
||||
toggleProfileDropState={toggleProfileDrop}
|
||||
@@ -120,12 +120,7 @@ function ClientMenuRouteWrapper({ children }: Props) {
|
||||
</div>
|
||||
|
||||
{!hideBottomNav && (
|
||||
<div
|
||||
ref={navContainerRef}
|
||||
data-menu-bottom-nav
|
||||
className="fixed bottom-7 left-0 right-0 z-45 px-4"
|
||||
style={{ willChange: "transform", transformOrigin: "center bottom" }}
|
||||
>
|
||||
<div ref={navContainerRef} data-menu-bottom-nav className="fixed bottom-7 left-0 right-0 z-45 px-4" style={{ willChange: "transform", transformOrigin: "center bottom" }}>
|
||||
<BottomNavBar onPagerClick={togglePager} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"Orders": "لیست سفارشات",
|
||||
"Transactions": "لیست تراکنش ها",
|
||||
"Games": "بازی و سرگرمی ها",
|
||||
"AI": "چی بخورم؟",
|
||||
"ShareWithFriends": "معرفی به دوستان",
|
||||
"ReportProblem": "گزارش اشکال",
|
||||
"InstallApp": "نصب اپلیکیشن",
|
||||
@@ -59,5 +60,29 @@
|
||||
"IOSStep3": "در صفحه بعدی روی دکمه «Add» در گوشه بالا بزنید.",
|
||||
"ChromeFallback": "اگر دکمه نصب را نمیبینید، یا اپلیکیشن قبلاً نصب شده است یا مرورگر هنوز امکان نصب را فعال نکرده است. کمی بعد دوباره تلاش کنید یا از منوی مرورگر گزینه نصب در صفحه اصلی را انتخاب کنید."
|
||||
},
|
||||
"SearchPlaceholder": "جستجو"
|
||||
"SearchPlaceholder": "جستجو",
|
||||
"AI": {
|
||||
"Heading": "چی بخورم؟",
|
||||
"BrandFallback": "دیمنو",
|
||||
"GuestName": "مهمان",
|
||||
"Greeting": {
|
||||
"morning": "صبح بخیر، {{name}}",
|
||||
"noon": "ظهر بخیر، {{name}}",
|
||||
"afternoon": "عصر بخیر، {{name}}",
|
||||
"evening": "شب بخیر، {{name}}"
|
||||
},
|
||||
"InputPlaceholder": "هر چی دوست داری بپرس…",
|
||||
"Suggestions": {
|
||||
"vegetarian": "غذای گیاهی چی دارید؟",
|
||||
"healthy": "غذای سالم چی دارید؟",
|
||||
"tasty": "یه غذای خوشمزه پیشنهاد بده.",
|
||||
"protein": "غذای پروتئینی پیشنهاد بده",
|
||||
"bestsellers": "پرفروشترین غذاها چیه؟",
|
||||
"spicy": "غذای تند پیشنهاد کن.",
|
||||
"quick": "یه غذای سریع برای من انتخاب کن",
|
||||
"dessert": "پیشنهادت برای دسر چیه؟",
|
||||
"group": "برای ۴ نفر چی سفارش بدیم"
|
||||
},
|
||||
"MockReply": "این نسخه فقط رابط کاربری است؛ بهزودی میتونم براتون از منو پیشنهاد واقعی بدم."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user