From e10177a788a7e577c3dc226b904b600be10dc0e6 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Thu, 23 Jul 2026 10:03:37 +0330 Subject: [PATCH] ui ai page --- public/assets/images/logo_ai.svg | 101 +++ src/app/[name]/(Main)/about/AboutPage.tsx | 259 +++--- src/app/[name]/(Main)/ai/AiPage.tsx | 112 +++ src/app/[name]/(Main)/ai/page.tsx | 3 + src/app/[name]/(Main)/ai/suggestions.ts | 29 + .../[name]/(Main)/components/MenuIndex.tsx | 6 +- src/components/menu/SideMenu.tsx | 807 +++++++++--------- .../wrapper/ClientMenuRouteWrapper.tsx | 15 +- src/locales/fa/common.json | 27 +- 9 files changed, 771 insertions(+), 588 deletions(-) create mode 100644 public/assets/images/logo_ai.svg create mode 100644 src/app/[name]/(Main)/ai/AiPage.tsx create mode 100644 src/app/[name]/(Main)/ai/page.tsx create mode 100644 src/app/[name]/(Main)/ai/suggestions.ts diff --git a/public/assets/images/logo_ai.svg b/public/assets/images/logo_ai.svg new file mode 100644 index 0000000..fc48a3b --- /dev/null +++ b/public/assets/images/logo_ai.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/[name]/(Main)/about/AboutPage.tsx b/src/app/[name]/(Main)/about/AboutPage.tsx index 9091c36..4f4f89c 100644 --- a/src/app/[name]/(Main)/about/AboutPage.tsx +++ b/src/app/[name]/(Main)/about/AboutPage.tsx @@ -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 = {}; 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 ( -
-
-
+
+
+
-

{restaurant.name}

- {restaurant.establishedYear && ( -

تاسیس: {restaurant.establishedYear}

- )} - {restaurant.tagNames && restaurant.tagNames.length > 0 && ( -

نوع محصولات: {restaurant.tagNames.join('، ')}

- )} +

{restaurant.name}

+ {restaurant.establishedYear &&

تاسیس: {restaurant.establishedYear}

} + {restaurant.tagNames && restaurant.tagNames.length > 0 &&

نوع محصولات: {restaurant.tagNames.join("، ")}

}
{restaurant.logo && (
- logo + logo
)}
{restaurant.description && ( -
+

درباره مجموعه

-

+

{restaurant.description}

{restaurant.images && restaurant.images.length > 0 && ( -
- - عکس های رستوران +
+ + عکس های رستوران
)}
@@ -129,60 +106,59 @@ function AboutPage() {
{(phoneList.length > 0 || restaurant.telegram || restaurant.whatsapp || restaurant.instagram) && ( -
-

ارتباط

-
+
+

ارتباط

+
{phoneList.map((phone) => ( - - - {phone} + + + + {phone} + ))} - {restaurant.telegram && - - + {restaurant.telegram && ( + + - } - {restaurant.whatsapp && - - + )} + {restaurant.whatsapp && ( + + - } - {restaurant.instagram && - - + )} + {restaurant.instagram && ( + + - } + )}
)} {restaurant.address && ( -
-

آدرس

-

{restaurant.address}

+
+

آدرس

+

{restaurant.address}

{restaurant.latitude && restaurant.longitude && ( -
- - موقعیت روی نقشه +
+ + موقعیت روی نقشه
)}
)} {schedules.length > 0 && getTodaySchedule() && ( -
+
- -
باز
-
-
امروز از ساعت {getTodaySchedule()}
+ +
باز
+
+
امروز از ساعت {getTodaySchedule()}
- +
)} @@ -192,17 +168,13 @@ function AboutPage() { {Object.entries(groupSchedulesByDay()) .sort(([a], [b]) => Number(a) - Number(b)) .map(([weekDay, daySchedules]) => ( -
-
- {getDayName(Number(weekDay))} -
-
+
+
{getDayName(Number(weekDay))}
+
{daySchedules.map((schedule, index) => ( {formatTime(schedule.openTime)} - {formatTime(schedule.closeTime)} - {index < daySchedules.length - 1 && '، '} + {index < daySchedules.length - 1 && "، "} ))}
@@ -211,15 +183,15 @@ function AboutPage() {
)}
- ) - } + ); + }; 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 ( -
-
-
- {averageRating.toFixed(1)} -
+
+
+
{averageRating.toFixed(1)}
- - - - - + + + + +
-
+
-

نظرات کاربران

-
@@ -287,8 +253,8 @@ function AboutPage() { sortedReviews.map((review) => (
)) ) : ( -

هنوز نظری ثبت نشده است

+

هنوز نظری ثبت نشده است

)}
@@ -313,29 +279,22 @@ function AboutPage() {
{i < sortings.length - 1 &&
} - ) + ); })}
- ) - } + ); + }; return ( -
+
- }> - - }> - + }> + }>
- ) + ); } - -export default AboutPage \ No newline at end of file +export default AboutPage; diff --git a/src/app/[name]/(Main)/ai/AiPage.tsx b/src/app/[name]/(Main)/ai/AiPage.tsx new file mode 100644 index 0000000..8fa7ef6 --- /dev/null +++ b/src/app/[name]/(Main)/ai/AiPage.tsx @@ -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([]); + + 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) => { + e.preventDefault(); + sendMessage(input); + }; + + return ( +
+

{t("Heading")}

+ +
+ {messages.length > 0 ? ( +
    + {messages.map((message) => ( +
  • +

    {message.content}

    +
  • + ))} +
+ ) : ( +
+
+ {brandName} +
+ +
+

{t(`Greeting.${getGreetingKey()}`, { name: userName })}

+ +
+ {AI_SUGGESTIONS.map(({ labelKey, icon: Icon }) => ( + + ))} +
+
+
+ )} + +
+ setInput(e.target.value)} + placeholder={t("InputPlaceholder")} + autoComplete="off" + className="h-full w-full bg-transparent text-[11px] outline-none placeholder:text-disabled-text" + /> + + +
+
+
+ ); +} + +export default AiPage; diff --git a/src/app/[name]/(Main)/ai/page.tsx b/src/app/[name]/(Main)/ai/page.tsx new file mode 100644 index 0000000..1c41868 --- /dev/null +++ b/src/app/[name]/(Main)/ai/page.tsx @@ -0,0 +1,3 @@ +'use client' + +export { default } from './AiPage' diff --git a/src/app/[name]/(Main)/ai/suggestions.ts b/src/app/[name]/(Main)/ai/suggestions.ts new file mode 100644 index 0000000..e02e171 --- /dev/null +++ b/src/app/[name]/(Main)/ai/suggestions.ts @@ -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 }, +] diff --git a/src/app/[name]/(Main)/components/MenuIndex.tsx b/src/app/[name]/(Main)/components/MenuIndex.tsx index 7c62b16..4b34c9d 100644 --- a/src/app/[name]/(Main)/components/MenuIndex.tsx +++ b/src/app/[name]/(Main)/components/MenuIndex.tsx @@ -183,9 +183,9 @@ const MenuIndex = () => {
- {selectedCategory === "0" ? "" : categories.find((c) => c.id === selectedCategory)?.title || ""} -
- diff --git a/src/components/menu/SideMenu.tsx b/src/components/menu/SideMenu.tsx index 3ab33f1..75e5eca 100644 --- a/src/components/menu/SideMenu.tsx +++ b/src/components/menu/SideMenu.tsx @@ -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> = [ - [ - { 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 | undefined; - nightModeState: boolean; - togglenightModeState: React.MouseEventHandler | undefined; + menuState: boolean; + toggleMenuState: React.MouseEventHandler | undefined; + nightModeState: boolean; + togglenightModeState: React.MouseEventHandler | undefined; }; type BeforeInstallPromptEvent = Event & { - prompt: () => Promise; - userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>; + prompt: () => Promise; + 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(null); + const { state: logoutModal, toggle: toggleLogoutModal } = useToggle(); + const { state: shareModal, toggle: toggleShareModal } = useToggle(); + const { state: installPwaModal, toggle: toggleInstallPwaModal } = useToggle(); + const [deferredPrompt, setDeferredPrompt] = useState(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(null); - const { state: logoutModal, toggle: toggleLogoutModal } = useToggle(); - const { state: shareModal, toggle: toggleShareModal } = useToggle(); - const { state: installPwaModal, toggle: toggleInstallPwaModal } = useToggle(); - const [deferredPrompt, setDeferredPrompt] = useState(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 ( -
- { - aboutData?.data?.logo && ( -
- logo -
- ) - } -
-
-
منو
-
- - -
- -
-
    - {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 ( -
  • - i.href === item.href)?.handler - : undefined - } - title={tMenu(item.title)} - icon={ - - - } - /> -
  • - ) - })} -
  • - }> - - -
  • -
-
-
- ) + 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 ( - <> -
- -
+
+ {aboutData?.data?.logo && ( +
+ logo +
+ )} +
+
+
منو
+
- {/* Logout Modal */} - + +
- {/* Share modal */} - -

- - {tShareModal('Heading')} -

-

- {tShareModal('Description')} -

- - - -
- - - -
-
- - {/* Install PWA modal */} - -

- - {tInstallPwaModal('Heading')} -

- {isPWAInstalled ? ( -

- {tInstallPwaModal('AlreadyInstalled')} -

- ) : isIOSDevice ? ( -
-

- {tInstallPwaModal('IOSDescription')} -

-
    -
  1. - - ۱ - - {tInstallPwaModal('IOSStep1')} -
  2. -
  3. - - ۲ - - {tInstallPwaModal('IOSStep2')} -
  4. -
  5. - - ۳ - - {tInstallPwaModal('IOSStep3')} -
  6. -
-
- ) : ( - <> -

- {tInstallPwaModal('Description')} -

- {deferredPrompt ? ( - - ) : ( -

- {tInstallPwaModal('ChromeFallback')} -

- )} - - )} - -
- +
+
    + {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 ( +
  • + i.href === item.href)?.handler : undefined} + title={tMenu(item.title)} + icon={ + + } + /> +
  • + ); + })} +
  • + }> + + +
  • +
+
+
); + }; + + return ( + <> +
+ +
+ + {/* Logout Modal */} + + + {/* Share modal */} + +

+ + {tShareModal("Heading")} +

+

{tShareModal("Description")}

+ + + +
+ + + +
+
+ + {/* Install PWA modal */} + +

+ + {tInstallPwaModal("Heading")} +

+ {isPWAInstalled ? ( +

{tInstallPwaModal("AlreadyInstalled")}

+ ) : isIOSDevice ? ( +
+

{tInstallPwaModal("IOSDescription")}

+
    +
  1. + ۱ + {tInstallPwaModal("IOSStep1")} +
  2. +
  3. + ۲ + {tInstallPwaModal("IOSStep2")} +
  4. +
  5. + ۳ + {tInstallPwaModal("IOSStep3")} +
  6. +
+
+ ) : ( + <> +

{tInstallPwaModal("Description")}

+ {deferredPrompt ? ( + + ) : ( +

{tInstallPwaModal("ChromeFallback")}

+ )} + + )} +
+ + ); } -export default SideMenu; \ No newline at end of file +export default SideMenu; diff --git a/src/components/wrapper/ClientMenuRouteWrapper.tsx b/src/components/wrapper/ClientMenuRouteWrapper.tsx index 5f0a409..1356849 100644 --- a/src/components/wrapper/ClientMenuRouteWrapper.tsx +++ b/src/components/wrapper/ClientMenuRouteWrapper.tsx @@ -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(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 (
-
+
{!hideBottomNav && ( -
+
)} diff --git a/src/locales/fa/common.json b/src/locales/fa/common.json index fe21ce0..32e34b4 100644 --- a/src/locales/fa/common.json +++ b/src/locales/fa/common.json @@ -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": "این نسخه فقط رابط کاربری است؛ به‌زودی می‌تونم براتون از منو پیشنهاد واقعی بدم." + } } \ No newline at end of file