ui ai page

This commit is contained in:
hamid zarghami
2026-07-23 10:03:37 +03:30
parent 81fc074708
commit e10177a788
9 changed files with 771 additions and 588 deletions
+109 -150
View File
@@ -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;
+112
View File
@@ -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;
+3
View File
@@ -0,0 +1,3 @@
'use client'
export { default } from './AiPage'
+29
View File
@@ -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
View File
@@ -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>
)}
+26 -1
View File
@@ -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": "این نسخه فقط رابط کاربری است؛ به‌زودی می‌تونم براتون از منو پیشنهاد واقعی بدم."
}
}