Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ff2d4adf3 | |||
| 80acaf743c | |||
| bace34a20f | |||
| b2d4f7848f | |||
| e8a950b0b6 | |||
| 3bb6ffac4e | |||
| ac049ef067 | |||
| 97b1cddf85 | |||
| ed5df3d7b7 | |||
| c565c0957f | |||
| d34a3a92f1 | |||
| de45b1f197 | |||
| ba82744219 | |||
| b5653906dc | |||
| d3ef8e1a98 | |||
| f6cfa369fc |
@@ -4,6 +4,7 @@ import { type FC } from 'react'
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import Seperator from '@/components/utils/Seperator'
|
import Seperator from '@/components/utils/Seperator'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface'
|
||||||
|
|
||||||
const GamePage: FC = () => {
|
const GamePage: FC = () => {
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ const GamePage: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-11 relative bg-primary text-white dark:bg-primary dark:text-primary-foreground h-[120px] rounded-[10px] p-4'>
|
<div className='mt-11 relative overflow-hidden bg-primary text-white dark:bg-primary dark:text-primary-foreground h-[120px] rounded-[10px] p-4'>
|
||||||
<div className='max-w-[99px]'>
|
<div className='max-w-[99px]'>
|
||||||
<div className='font-bold'>My Games</div>
|
<div className='font-bold'>My Games</div>
|
||||||
<div className='mt-1'>
|
<div className='mt-1'>
|
||||||
@@ -46,7 +47,7 @@ const GamePage: FC = () => {
|
|||||||
<div className='mt-8 flex flex-col gap-4'>
|
<div className='mt-8 flex flex-col gap-4'>
|
||||||
<div
|
<div
|
||||||
style={{ direction: 'ltr' }}
|
style={{ direction: 'ltr' }}
|
||||||
className='bg-white dark:bg-container shadow-sm rounded-[10px] p-2 flex justify-between items-center cursor-pointer border border-transparent dark:border-border/50'
|
className={glassSurfaceCard('rounded-[10px] p-2 flex justify-between items-center cursor-pointer')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const currentPath = window.location.pathname
|
const currentPath = window.location.pathname
|
||||||
const basePath = currentPath.split('/game')[0]
|
const basePath = currentPath.split('/game')[0]
|
||||||
@@ -76,7 +77,7 @@ const GamePage: FC = () => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
style={{ direction: 'ltr' }}
|
style={{ direction: 'ltr' }}
|
||||||
className='bg-white dark:bg-container shadow-sm rounded-[10px] p-2 flex justify-between items-center cursor-pointer border border-transparent dark:border-border/50'
|
className={glassSurfaceCard('rounded-[10px] p-2 flex justify-between items-center cursor-pointer')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const currentPath = window.location.pathname
|
const currentPath = window.location.pathname
|
||||||
const basePath = currentPath.split('/game')[0]
|
const basePath = currentPath.split('/game')[0]
|
||||||
@@ -106,7 +107,7 @@ const GamePage: FC = () => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
style={{ direction: 'ltr' }}
|
style={{ direction: 'ltr' }}
|
||||||
className='bg-white dark:bg-container shadow-sm rounded-[10px] p-2 flex justify-between items-center cursor-pointer border border-transparent dark:border-border/50'
|
className={glassSurfaceCard('rounded-[10px] p-2 flex justify-between items-center cursor-pointer')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const currentPath = window.location.pathname
|
const currentPath = window.location.pathname
|
||||||
const basePath = currentPath.split('/game')[0]
|
const basePath = currentPath.split('/game')[0]
|
||||||
|
|||||||
@@ -2,21 +2,11 @@
|
|||||||
|
|
||||||
import ClientSideWrapper from '@/components/wrapper/ClientSideWrapper';
|
import ClientSideWrapper from '@/components/wrapper/ClientSideWrapper';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
function DialogsLayout({ children }: { children: React.ReactNode }) {
|
function DialogsLayout({ children }: { children: React.ReactNode }) {
|
||||||
const pathname = usePathname();
|
|
||||||
const isCheckoutPage = pathname?.includes('/checkout/');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClientSideWrapper>
|
<ClientSideWrapper>
|
||||||
<main className={clsx(
|
<main className='h-full bg-background dark:bg-background w-full overflow-y-auto noscrollbar pt-4 pb-6 px-6'>
|
||||||
'h-full dark:bg-background w-full overflow-y-auto noscrollbar pt-4 pb-6 px-6',
|
|
||||||
isCheckoutPage
|
|
||||||
? 'bg-background dark:bg-background'
|
|
||||||
: 'bg-container lg:bg-background'
|
|
||||||
)}>
|
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</ClientSideWrapper>
|
</ClientSideWrapper>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useGetNotifications, useReadAllNotifications } from './hooks/useNotific
|
|||||||
import type { Notification } from './types/Types';
|
import type { Notification } from './types/Types';
|
||||||
import { useGetNotificationsCount } from '../../(Main)/hooks/useMenuData';
|
import { useGetNotificationsCount } from '../../(Main)/hooks/useMenuData';
|
||||||
import Prompt from '@/components/utils/Prompt';
|
import Prompt from '@/components/utils/Prompt';
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
type Props = object
|
type Props = object
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ export default function NotificationsIndex({ }: Props) {
|
|||||||
const formattedTime = formatTime(notification.createdAt);
|
const formattedTime = formatTime(notification.createdAt);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={notification.id} className='bg-container py-5 px-4 rounded-container shadow-lg'>
|
<li key={notification.id} className={glassSurfaceCard('py-5 px-4')}>
|
||||||
{/* <h5 className='font-medium text-sm'>{notification.title}</h5> */}
|
{/* <h5 className='font-medium text-sm'>{notification.title}</h5> */}
|
||||||
<p className='text-xs mt-2 leading-5'>{notification.content}</p>
|
<p className='text-xs mt-2 leading-5'>{notification.content}</p>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Link from 'next/link';
|
|||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||||
import { useCheckoutStore } from '../../store/Store';
|
import { useCheckoutStore } from '../../store/Store';
|
||||||
import useToggle from '@/hooks/helpers/useToggle';
|
import useToggle from '@/hooks/helpers/useToggle';
|
||||||
import { AddressSelectionModal } from './AddressSelectionModal';
|
import { AddressSelectionModal } from './AddressSelectionModal';
|
||||||
@@ -66,7 +67,7 @@ export const AddressSection = () => {
|
|||||||
// اگر آدرس ندارد
|
// اگر آدرس ندارد
|
||||||
if (addresses.length === 0) {
|
if (addresses.length === 0) {
|
||||||
return (
|
return (
|
||||||
<section className="bg-container rounded-container box-shadow-normal p-4">
|
<section className={glassSurfaceCard('p-4')}>
|
||||||
<div className="py-3">
|
<div className="py-3">
|
||||||
<h3 className="text-sm2 font-medium leading-5 mb-4">{t("SectionAddress.Title")}</h3>
|
<h3 className="text-sm2 font-medium leading-5 mb-4">{t("SectionAddress.Title")}</h3>
|
||||||
<p className='text-sm2 text-disabled-text mb-4'>
|
<p className='text-sm2 text-disabled-text mb-4'>
|
||||||
@@ -89,7 +90,7 @@ export const AddressSection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="bg-container rounded-container box-shadow-normal p-4">
|
<section className={glassSurfaceCard('p-4')}>
|
||||||
<div className="py-3">
|
<div className="py-3">
|
||||||
<div className='w-full flex justify-between items-center gap-2'>
|
<div className='w-full flex justify-between items-center gap-2'>
|
||||||
<h3 className="text-sm2 font-medium leading-5 inline-block">{t("SectionAddress.Title")}</h3>
|
<h3 className="text-sm2 font-medium leading-5 inline-block">{t("SectionAddress.Title")}</h3>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import InputField from '@/components/input/InputField';
|
import InputField from '@/components/input/InputField';
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||||
import { useCheckoutStore } from '../../store/Store';
|
import { useCheckoutStore } from '../../store/Store';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ export const CarAddressSection = () => {
|
|||||||
}, [carAddress, setCarAddress]);
|
}, [carAddress, setCarAddress]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-container rounded-container box-shadow-normal p-4">
|
<section className={glassSurfaceCard('p-4')}>
|
||||||
<div className="py-3">
|
<div className="py-3">
|
||||||
<h3 className="text-sm2 font-medium leading-5 mb-4">
|
<h3 className="text-sm2 font-medium leading-5 mb-4">
|
||||||
اطلاعات خودرو
|
اطلاعات خودرو
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useApplyCoupon, useRemoveCoupon } from '../../hooks/useOrderData';
|
|||||||
import Button from '@/components/button/PrimaryButton';
|
import Button from '@/components/button/PrimaryButton';
|
||||||
import { extractErrorMessage } from '@/lib/func';
|
import { extractErrorMessage } from '@/lib/func';
|
||||||
import { toast } from '@/components/Toast';
|
import { toast } from '@/components/Toast';
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useGetCartItems } from '@/app/[name]/(Main)/cart/hooks/useCartData';
|
import { useGetCartItems } from '@/app/[name]/(Main)/cart/hooks/useCartData';
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ export const CouponSection = ({
|
|||||||
}, [cartItems, onCouponCodeChange]);
|
}, [cartItems, onCouponCodeChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-container rounded-container box-shadow-normal p-4">
|
<section className={glassSurfaceCard('p-4')}>
|
||||||
<div className="py-3">
|
<div className="py-3">
|
||||||
<h3 className="text-sm2 font-medium leading-5">{t("SectionCoupon.Title")}</h3>
|
<h3 className="text-sm2 font-medium leading-5">{t("SectionCoupon.Title")}</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|||||||
import { extractErrorMessage } from "@/lib/func";
|
import { extractErrorMessage } from "@/lib/func";
|
||||||
import { Card, Icon, Wallet2 } from "iconsax-react";
|
import { Card, Icon, Wallet2 } from "iconsax-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { glassSurfaceCard } from "@/lib/styles/glassSurface";
|
||||||
import { useCallback, useMemo, useRef } from "react";
|
import { useCallback, useMemo, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { PaymentMethodEnum } from "../../enum/Enum";
|
import { PaymentMethodEnum } from "../../enum/Enum";
|
||||||
@@ -121,7 +122,7 @@ export const PaymentSection = ({ paymentType, onPaymentTypeChange }: PaymentSect
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-container rounded-container box-shadow-normal p-4">
|
<section className={glassSurfaceCard('p-4')}>
|
||||||
<div className="py-3">
|
<div className="py-3">
|
||||||
<h3 className="text-sm2 font-medium leading-5">{tOrderDetail("SectionPayment.Title")}</h3>
|
<h3 className="text-sm2 font-medium leading-5">{tOrderDetail("SectionPayment.Title")}</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useGetShipmentMethod } from '../../hooks/useOrderData';
|
import { useGetShipmentMethod } from '../../hooks/useOrderData';
|
||||||
import { DeliveryMethod } from '../../types/Types';
|
import { DeliveryMethod } from '../../types/Types';
|
||||||
import { useCheckoutStore } from '../../store/Store';
|
import { useCheckoutStore } from '../../store/Store';
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
type ShippingSectionProps = {
|
type ShippingSectionProps = {
|
||||||
deliveryType: string;
|
deliveryType: string;
|
||||||
@@ -57,7 +58,7 @@ export const ShippingSection = ({ deliveryType, onDeliveryTypeChange }: Shipping
|
|||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-container rounded-container box-shadow-normal p-4">
|
<section className={glassSurfaceCard('p-4')}>
|
||||||
<div className="py-3">
|
<div className="py-3">
|
||||||
<h3 className="text-sm2 font-medium leading-5">{tOrderDetail("SectionShipping.Title")}</h3>
|
<h3 className="text-sm2 font-medium leading-5">{tOrderDetail("SectionShipping.Title")}</h3>
|
||||||
<Combobox
|
<Combobox
|
||||||
@@ -80,4 +81,3 @@ export const ShippingSection = ({ deliveryType, onDeliveryTypeChange }: Shipping
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { MinusIcon, PlusIcon } from 'lucide-react';
|
import { MinusIcon, PlusIcon } from 'lucide-react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
type TableSectionProps = {
|
type TableSectionProps = {
|
||||||
tableNumber: number;
|
tableNumber: number;
|
||||||
@@ -14,7 +15,7 @@ export const TableSection = ({ tableNumber, onIncrement, onDecrement }: TableSec
|
|||||||
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
|
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-container rounded-container box-shadow-normal p-4 py-2">
|
<section className={glassSurfaceCard('p-4 py-2')}>
|
||||||
<div className="py-3">
|
<div className="py-3">
|
||||||
<div className='w-full grid grid-cols-2 justify-between items-center gap-2'>
|
<div className='w-full grid grid-cols-2 justify-between items-center gap-2'>
|
||||||
<h3 className="text-sm2 font-medium leading-5 inline-block">{t("SectionTable.Title")}</h3>
|
<h3 className="text-sm2 font-medium leading-5 inline-block">{t("SectionTable.Title")}</h3>
|
||||||
@@ -22,7 +23,7 @@ export const TableSection = ({ tableNumber, onIncrement, onDecrement }: TableSec
|
|||||||
<div className='w-full flex justify-end'>
|
<div className='w-full flex justify-end'>
|
||||||
<motion.div
|
<motion.div
|
||||||
whileTap={{ scale: 1.05 }}
|
whileTap={{ scale: 1.05 }}
|
||||||
className="bg-background active:drop-shadow-xs max-w-[132px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2"
|
className="bg-background pattern-secondary-bg active:drop-shadow-xs max-w-[132px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={onIncrement}
|
onClick={onIncrement}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import React, { useEffect, useState } from 'react'
|
|||||||
import { useVerify } from '../hooks/useVerifyData';
|
import { useVerify } from '../hooks/useVerifyData';
|
||||||
import { toast } from '@/components/Toast';
|
import { toast } from '@/components/Toast';
|
||||||
import { VerifyData } from '../types/Types';
|
import { VerifyData } from '../types/Types';
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
type Props = object
|
type Props = object
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ export default function VerifyIndex({ }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className='h-full bg-inherit relative flex flex-col lg:gap-4'>
|
<div className='h-full bg-inherit relative flex flex-col lg:gap-4'>
|
||||||
<div className='flex-1 flex flex-col items-center justify-center'>
|
<div className='flex-1 flex flex-col items-center justify-center'>
|
||||||
<div className='bg-container py-5 px-2 rounded-container shadow-lg w-full'>
|
<div className={glassSurfaceCard('py-5 px-2 w-full')}>
|
||||||
<div className={`items-center gap-2 rounded-2xl h-35 flex flex-col justify-center ${isError ? 'bg-red-50' : 'bg-gray-50'}`}>
|
<div className={`items-center gap-2 rounded-2xl h-35 flex flex-col justify-center ${isError ? 'bg-red-50' : 'bg-gray-50'}`}>
|
||||||
{isError ? (
|
{isError ? (
|
||||||
<CloseCircle className='size-10 stroke-red-500' variant='TwoTone' />
|
<CloseCircle className='size-10 stroke-red-500' variant='TwoTone' />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import PlusIcon from "@/components/icons/PlusIcon";
|
|||||||
import { toast, toastLoginRequired } from "@/components/Toast";
|
import { toast, toastLoginRequired } from "@/components/Toast";
|
||||||
import { getToken } from "@/lib/api/func";
|
import { getToken } from "@/lib/api/func";
|
||||||
import { ef } from "@/lib/helpers/utfNumbers";
|
import { ef } from "@/lib/helpers/utfNumbers";
|
||||||
|
import { glassSurface, glassSurfaceCard, glassSurfaceFlat } from "@/lib/styles/glassSurface";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { ArrowLeft, Clock, Cup, Heart, TruckTick } from "iconsax-react";
|
import { ArrowLeft, Clock, Cup, Heart, TruckTick } from "iconsax-react";
|
||||||
@@ -105,18 +106,14 @@ function FoodPage({}: Props) {
|
|||||||
: Array.isArray(foodData.images) && foodData.images.length > 0
|
: Array.isArray(foodData.images) && foodData.images.length > 0
|
||||||
? typeof foodData.images[0] === "string"
|
? typeof foodData.images[0] === "string"
|
||||||
? foodData.images[0]
|
? foodData.images[0]
|
||||||
: typeof foodData.images[0] === "object" &&
|
: typeof foodData.images[0] === "object" && foodData.images[0] !== null && "url" in foodData.images[0]
|
||||||
foodData.images[0] !== null &&
|
|
||||||
"url" in foodData.images[0]
|
|
||||||
? (foodData.images[0] as { url: string }).url
|
? (foodData.images[0] as { url: string }).url
|
||||||
: "/assets/images/no-image.png"
|
: "/assets/images/no-image.png"
|
||||||
: "/assets/images/no-image.png";
|
: "/assets/images/no-image.png";
|
||||||
const categoryName = foodData.category?.title;
|
const categoryName = foodData.category?.title;
|
||||||
const prepareTime = foodData.prepareTime || 0;
|
const prepareTime = foodData.prepareTime || 0;
|
||||||
const price = foodData.price || 0;
|
const price = foodData.price || 0;
|
||||||
const content = Array.isArray(foodData.content)
|
const content = Array.isArray(foodData.content) ? foodData.content.join("، ") : foodData.content || foodData.desc || "";
|
||||||
? foodData.content.join("، ")
|
|
||||||
: foodData.content || foodData.desc || "";
|
|
||||||
|
|
||||||
const handleBackToMenu = () => {
|
const handleBackToMenu = () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
@@ -135,7 +132,9 @@ function FoodPage({}: Props) {
|
|||||||
!isAvailable && "opacity-50",
|
!isAvailable && "opacity-50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative w-full lg:h-full min-h-0 not-lg:bg-container rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1">
|
<div
|
||||||
|
className={glassSurface("relative w-full lg:h-full min-h-0 rounded-2xl overflow-hidden lg:rounded-r-none lg:order-1 lg:bg-transparent! lg:border-0! lg:shadow-none! lg:[backdrop-filter:none]")}
|
||||||
|
>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
className="lg:object-cover lg:h-full object-cover bg-[#F2F2F9] w-full"
|
className="lg:object-cover lg:h-full object-cover bg-[#F2F2F9] w-full"
|
||||||
@@ -146,34 +145,20 @@ function FoodPage({}: Props) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="absolute top-4 left-0 pe-5.5 ps-7 w-full inline-flex justify-between items-center">
|
<div className="absolute top-4 left-0 pe-5.5 ps-7 w-full inline-flex justify-between items-center">
|
||||||
<div className="bg-container whitespace-nowrap rounded-[34px] py-1.5 px-4 w-min text-xs text-disabled2 dark:text-disabled-text font-bold">
|
<div className={glassSurfaceFlat("whitespace-nowrap rounded-[34px] py-1.5 px-4 w-min text-xs text-disabled2 dark:text-disabled-text font-bold")}>{categoryName || "-"}</div>
|
||||||
{categoryName || "-"}
|
<button onClick={handleBackToMenu} className={glassSurfaceFlat("p-2 rounded-full")}>
|
||||||
</div>
|
<ArrowLeft size={18} className="stroke-primary dark:stroke-foreground" />
|
||||||
<button
|
|
||||||
onClick={handleBackToMenu}
|
|
||||||
className="p-2 rounded-full bg-container/40"
|
|
||||||
>
|
|
||||||
<ArrowLeft
|
|
||||||
size={18}
|
|
||||||
className="stroke-primary dark:stroke-foreground"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative px-6 pt-6 pb-7.5 lg:flex lg:flex-col lg:h-full lg:min-h-0 bg-container w-full rounded-3xl overflow-hidden not-lg:-translate-y-6 lg:rounded-l-none lg:overflow-y-auto">
|
<div
|
||||||
|
className={glassSurfaceCard("relative px-6 pt-6 pb-7.5 lg:flex lg:flex-col lg:h-full lg:min-h-0 w-full overflow-hidden not-lg:-translate-y-6 lg:rounded-l-none lg:overflow-y-auto rounded-3xl")}
|
||||||
|
>
|
||||||
<div className="w-full inline-flex justify-between items-center shrink-0">
|
<div className="w-full inline-flex justify-between items-center shrink-0">
|
||||||
<h5 className="text-base font-bold">{foodName}</h5>
|
<h5 className="text-base font-bold">{foodName}</h5>
|
||||||
<button
|
<button onClick={handleToggleFavorite} className="p-2 bg-[#EAECF0] dark:bg-neutral-600 rounded-lg">
|
||||||
onClick={handleToggleFavorite}
|
<Heart variant={isFavorite ? "Bold" : "Outline"} size={24} color="currentColor" className={isFavorite ? "fill-primary dark:fill-foreground" : ""} />
|
||||||
className="p-2 bg-[#EAECF0] dark:bg-neutral-600 rounded-lg"
|
|
||||||
>
|
|
||||||
<Heart
|
|
||||||
variant={isFavorite ? "Bold" : "Outline"}
|
|
||||||
size={24}
|
|
||||||
color="currentColor"
|
|
||||||
className={isFavorite ? "fill-primary dark:fill-foreground" : ""}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -181,10 +166,7 @@ function FoodPage({}: Props) {
|
|||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Clock size={14} className="stroke-disabled-text" />
|
<Clock size={14} className="stroke-disabled-text" />
|
||||||
<span className="text-disabled-text">
|
<span className="text-disabled-text">زمان پخت و آماده سازی: {prepareTime > 0 ? `${ef(String(prepareTime))} دقیقه` : "-"}</span>
|
||||||
زمان پخت و آماده سازی:{" "}
|
|
||||||
{prepareTime > 0 ? `${ef(String(prepareTime))} دقیقه` : "-"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-2">
|
<div className="flex items-center gap-2 mt-2">
|
||||||
<Cup size={14} className="stroke-disabled-text" />
|
<Cup size={14} className="stroke-disabled-text" />
|
||||||
@@ -194,18 +176,9 @@ function FoodPage({}: Props) {
|
|||||||
<div>۰</div>
|
<div>۰</div>
|
||||||
<span>امتیاز برای هر بار خرید</span>
|
<span>امتیاز برای هر بار خرید</span>
|
||||||
</>
|
</>
|
||||||
) : about?.data?.score?.purchaseScore &&
|
) : about?.data?.score?.purchaseScore && about?.data?.score?.purchaseAmount ? (
|
||||||
about?.data?.score?.purchaseAmount ? (
|
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>{ef(Math.round(foodData?.price * (Number(about.data.score.purchaseScore) / Number(about.data.score.purchaseAmount))).toLocaleString("en-US"))}</div>
|
||||||
{ef(
|
|
||||||
Math.round(
|
|
||||||
foodData?.price *
|
|
||||||
(Number(about.data.score.purchaseScore) /
|
|
||||||
Number(about.data.score.purchaseAmount)),
|
|
||||||
).toLocaleString("en-US"),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span>امتیاز برای هر بار خرید</span>
|
<span>امتیاز برای هر بار خرید</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -215,9 +188,7 @@ function FoodPage({}: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-2">
|
<div className="flex items-center gap-2 mt-2">
|
||||||
<TruckTick size={14} className="stroke-disabled-text" />
|
<TruckTick size={14} className="stroke-disabled-text" />
|
||||||
<span className="text-disabled-text">
|
<span className="text-disabled-text">{foodData.pickupServe ? "ارسال با پیک" : "-"}</span>
|
||||||
{foodData.pickupServe ? "ارسال با پیک" : "-"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -231,41 +202,26 @@ function FoodPage({}: Props) {
|
|||||||
<span dir="ltr">{ef(price.toLocaleString("en-US"))} T</span>
|
<span dir="ltr">{ef(price.toLocaleString("en-US"))} T</span>
|
||||||
<motion.div
|
<motion.div
|
||||||
whileTap={isAvailable ? { scale: 1.05 } : undefined}
|
whileTap={isAvailable ? { scale: 1.05 } : undefined}
|
||||||
className="bg-background active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
|
className="bg-background pattern-secondary-bg active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
|
||||||
>
|
>
|
||||||
{quantity <= 0 ? (
|
{quantity <= 0 ? (
|
||||||
<button
|
<button onClick={handleAddToCart} disabled={!isAvailable} className="inline-flex w-full justify-center items-center gap-2 disabled:pointer-events-none disabled:opacity-40">
|
||||||
onClick={handleAddToCart}
|
|
||||||
disabled={!isAvailable}
|
|
||||||
className="inline-flex w-full justify-center items-center gap-2 disabled:pointer-events-none disabled:opacity-40"
|
|
||||||
>
|
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
<div className="text-sm2 pt-0.5 font-normal text-foreground">
|
<div className="text-sm2 pt-0.5 font-normal text-foreground">افزودن</div>
|
||||||
افزودن
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={handleAddToCart}
|
onClick={handleAddToCart}
|
||||||
disabled={!isAvailable}
|
disabled={!isAvailable}
|
||||||
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors disabled:opacity-40 disabled:pointer-events-none"
|
className={glassSurfaceFlat("hover:brightness-95 active:brightness-90 rounded-sm p-2 transition-all disabled:opacity-40 disabled:pointer-events-none")}
|
||||||
>
|
>
|
||||||
<PlusIcon className="text-foreground" />
|
<PlusIcon className="text-foreground" />
|
||||||
</button>
|
</button>
|
||||||
<motion.div
|
<motion.div key={quantity} initial={{ scale: 1 }} animate={{ scale: [1.2, 0.95, 1] }} transition={{ duration: 0.3 }} className="text-sm2 pt-0.5 font-semibold">
|
||||||
key={quantity}
|
|
||||||
initial={{ scale: 1 }}
|
|
||||||
animate={{ scale: [1.2, 0.95, 1] }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
className="text-sm2 pt-0.5 font-semibold"
|
|
||||||
>
|
|
||||||
{quantity}
|
{quantity}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<button
|
<button onClick={handleRemoveFromCart} className={glassSurfaceFlat("hover:brightness-95 active:brightness-90 rounded-sm p-2 transition-all")}>
|
||||||
onClick={handleRemoveFromCart}
|
|
||||||
className="bg-container hover:bg-container/60 active:bg-container/30 rounded-sm p-2 transition-colors"
|
|
||||||
>
|
|
||||||
<MinusIcon className="text-foreground" />
|
<MinusIcon className="text-foreground" />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import Image from 'next/image';
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useGetAbout, useGetReviews, useGetSchedules } from './hooks/useAboutData';
|
import { useGetAbout, useGetReviews, useGetSchedules } from './hooks/useAboutData';
|
||||||
import AboutSkeleton from './components/AboutSkeleton';
|
import AboutSkeleton from './components/AboutSkeleton';
|
||||||
|
import { glassSurfaceCard, glassSurfaceCardFlat, glassSurfaceFlat } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
const sortings = [
|
const sortings = [
|
||||||
'جدیدترین',
|
'جدیدترین',
|
||||||
@@ -81,7 +82,7 @@ function AboutPage() {
|
|||||||
return (
|
return (
|
||||||
<section aria-labelledby="about-title" className='py-4'>
|
<section aria-labelledby="about-title" className='py-4'>
|
||||||
<section
|
<section
|
||||||
className="bg-container rounded-container shadow-container p-4">
|
className={glassSurfaceCard('p-4')}>
|
||||||
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-[25px]">
|
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-[25px]">
|
||||||
<div className="">
|
<div className="">
|
||||||
<h2 className='text-sm2 font-bold leading-5'>{restaurant.name}</h2>
|
<h2 className='text-sm2 font-bold leading-5'>{restaurant.name}</h2>
|
||||||
@@ -123,7 +124,7 @@ function AboutPage() {
|
|||||||
|
|
||||||
{(restaurant.phone || restaurant.telegram || restaurant.whatsapp || restaurant.instagram) && (
|
{(restaurant.phone || restaurant.telegram || restaurant.whatsapp || restaurant.instagram) && (
|
||||||
<section
|
<section
|
||||||
className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center">
|
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>
|
<h2 className='text-sm2 font-medium leading-5'>ارتباط</h2>
|
||||||
<div className='col-span-1 text-center flex justify-center gap-4'>
|
<div className='col-span-1 text-center flex justify-center gap-4'>
|
||||||
{restaurant.phone &&
|
{restaurant.phone &&
|
||||||
@@ -151,7 +152,7 @@ function AboutPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{restaurant.address && (
|
{restaurant.address && (
|
||||||
<section className="bg-container rounded-container shadow-container px-4 pt-6 pb-6 mt-4">
|
<section className={glassSurfaceCard('px-4 pt-6 pb-6 mt-4')}>
|
||||||
<h2 className='text-sm2 font-medium leading-5'>آدرس</h2>
|
<h2 className='text-sm2 font-medium leading-5'>آدرس</h2>
|
||||||
<p className='text-sm2 mt-[9px] leading-5'>{restaurant.address}</p>
|
<p className='text-sm2 mt-[9px] leading-5'>{restaurant.address}</p>
|
||||||
{restaurant.latitude && restaurant.longitude && (
|
{restaurant.latitude && restaurant.longitude && (
|
||||||
@@ -166,7 +167,7 @@ function AboutPage() {
|
|||||||
{schedules.length > 0 && getTodaySchedule() && (
|
{schedules.length > 0 && getTodaySchedule() && (
|
||||||
<section
|
<section
|
||||||
aria-label='ساعات کاری'
|
aria-label='ساعات کاری'
|
||||||
className="bg-container rounded-container shadow-container py-6 px-4 mt-4 flex justify-between items-center">
|
className={glassSurfaceCard('py-6 px-4 mt-4 flex justify-between items-center')}>
|
||||||
<div className="flex items-center gap-2 justify-start">
|
<div className="flex items-center gap-2 justify-start">
|
||||||
<Clock size={16} className='stroke-disabled-text' />
|
<Clock size={16} className='stroke-disabled-text' />
|
||||||
<div className='text-sm2 font-medium leading-5 mt-0.5 text-[#8C90A3]'>باز</div>
|
<div className='text-sm2 font-medium leading-5 mt-0.5 text-[#8C90A3]'>باز</div>
|
||||||
@@ -186,8 +187,7 @@ function AboutPage() {
|
|||||||
.map(([weekDay, daySchedules]) => (
|
.map(([weekDay, daySchedules]) => (
|
||||||
<div
|
<div
|
||||||
key={weekDay}
|
key={weekDay}
|
||||||
style={{ boxShadow: '0px 0px 14px 0px #0000000F' }}
|
className={glassSurfaceCardFlat('leading-5 py-6 px-4 mt-4 flex justify-between items-center')}>
|
||||||
className="bg-[#F6F6FA] dark:bg-border rounded-container leading-5 py-6 px-4 mt-4 flex justify-between items-center">
|
|
||||||
<div className="text-sm2">
|
<div className="text-sm2">
|
||||||
{getDayName(Number(weekDay))}
|
{getDayName(Number(weekDay))}
|
||||||
</div>
|
</div>
|
||||||
@@ -254,7 +254,7 @@ function AboutPage() {
|
|||||||
<section aria-labelledby="reviews-title" className='py-4'>
|
<section aria-labelledby="reviews-title" className='py-4'>
|
||||||
<section
|
<section
|
||||||
aria-label='امتیاز کاربران'
|
aria-label='امتیاز کاربران'
|
||||||
className="bg-container rounded-container shadow-container p-4 py-6 grid grid-cols-2 items-center">
|
className={glassSurfaceCard('p-4 py-6 grid grid-cols-2 items-center')}>
|
||||||
<div className="text-center font-bold text-5xl">
|
<div className="text-center font-bold text-5xl">
|
||||||
{averageRating.toFixed(1)}
|
{averageRating.toFixed(1)}
|
||||||
</div>
|
</div>
|
||||||
@@ -267,10 +267,10 @@ function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4">
|
<section className={glassSurfaceCard('pt-3 pb-3.5 px-[16px] mt-4')}>
|
||||||
<div className="flex justify-between items-center border-b-[1.5px] border-border pb-2">
|
<div className="flex justify-between items-center border-b-[1.5px] border-border pb-2">
|
||||||
<h2 className='text-sm2 font-medium leading-5'>نظرات کاربران</h2>
|
<h2 className='text-sm2 font-medium leading-5'>نظرات کاربران</h2>
|
||||||
<button onClick={toggleSortingModal} className="rounded-xl h-8 bg-container ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]">
|
<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' />
|
<EqualizerIcon className='dark:text-white' />
|
||||||
<span className="text-xs leading-5 font-medium">{sortings[+sorting]}</span>
|
<span className="text-xs leading-5 font-medium">{sortings[+sorting]}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import TabContainer from "@/components/tab/TabContainer";
|
import TabContainer from "@/components/tab/TabContainer";
|
||||||
import { TabHeader } from "@/components/tab/TabHeader";
|
import { TabHeader } from "@/components/tab/TabHeader";
|
||||||
import { InfoCircle, Star1 } from 'iconsax-react';
|
import { InfoCircle, Star1 } from 'iconsax-react';
|
||||||
|
import { glassSurfaceCard, glassSurfaceCardFlat } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
const AboutSkeleton = () => {
|
const AboutSkeleton = () => {
|
||||||
const firstTabSkeleton = () => (
|
const firstTabSkeleton = () => (
|
||||||
<section className='py-4'>
|
<section className='py-4'>
|
||||||
{/* اطلاعات رستوران */}
|
{/* اطلاعات رستوران */}
|
||||||
<section className="bg-container rounded-container shadow-container p-4">
|
<section className={glassSurfaceCard('p-4')}>
|
||||||
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-[25px]">
|
<div className="flex justify-between items-center border-b-[1.5px] border-gray-200 pb-[25px]">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Skeleton className="h-6 w-32 rounded-md mb-4" />
|
<Skeleton className="h-6 w-32 rounded-md mb-4" />
|
||||||
@@ -31,7 +32,7 @@ const AboutSkeleton = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ارتباط */}
|
{/* ارتباط */}
|
||||||
<section className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center">
|
<section className={glassSurfaceCard('pt-3 pb-3.5 px-[16px] mt-4 grid grid-cols-3 items-center')}>
|
||||||
<Skeleton className="h-5 w-16 rounded-md" />
|
<Skeleton className="h-5 w-16 rounded-md" />
|
||||||
<div className='col-span-1 text-center flex justify-center gap-4'>
|
<div className='col-span-1 text-center flex justify-center gap-4'>
|
||||||
{[...Array(3)].map((_, index) => (
|
{[...Array(3)].map((_, index) => (
|
||||||
@@ -41,7 +42,7 @@ const AboutSkeleton = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* آدرس */}
|
{/* آدرس */}
|
||||||
<section className="bg-container rounded-container shadow-container px-4 pt-6 pb-6 mt-4">
|
<section className={glassSurfaceCard('px-4 pt-6 pb-6 mt-4')}>
|
||||||
<Skeleton className="h-5 w-12 rounded-md mb-[9px]" />
|
<Skeleton className="h-5 w-12 rounded-md mb-[9px]" />
|
||||||
<Skeleton className="h-4 w-full rounded-md mb-2" />
|
<Skeleton className="h-4 w-full rounded-md mb-2" />
|
||||||
<Skeleton className="h-4 w-3/4 rounded-md mb-[23px]" />
|
<Skeleton className="h-4 w-3/4 rounded-md mb-[23px]" />
|
||||||
@@ -52,7 +53,7 @@ const AboutSkeleton = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ساعات کاری امروز */}
|
{/* ساعات کاری امروز */}
|
||||||
<section className="bg-container rounded-container shadow-container py-6 px-4 mt-4 flex justify-between items-center">
|
<section className={glassSurfaceCard('py-6 px-4 mt-4 flex justify-between items-center')}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Skeleton className="w-4 h-4 rounded-full" />
|
<Skeleton className="w-4 h-4 rounded-full" />
|
||||||
<Skeleton className="h-5 w-8 rounded-md" />
|
<Skeleton className="h-5 w-8 rounded-md" />
|
||||||
@@ -66,7 +67,7 @@ const AboutSkeleton = () => {
|
|||||||
{[...Array(3)].map((_, index) => (
|
{[...Array(3)].map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="bg-[#F6F6FA] dark:bg-border rounded-container leading-5 py-6 px-4 mt-4 flex justify-between items-center">
|
className={glassSurfaceCardFlat('leading-5 py-6 px-4 mt-4 flex justify-between items-center')}>
|
||||||
<Skeleton className="h-5 w-20 rounded-md" />
|
<Skeleton className="h-5 w-20 rounded-md" />
|
||||||
<Skeleton className="h-5 w-24 rounded-md" />
|
<Skeleton className="h-5 w-24 rounded-md" />
|
||||||
</div>
|
</div>
|
||||||
@@ -77,7 +78,7 @@ const AboutSkeleton = () => {
|
|||||||
const secondTabSkeleton = () => (
|
const secondTabSkeleton = () => (
|
||||||
<section className='py-4'>
|
<section className='py-4'>
|
||||||
{/* امتیاز کاربران */}
|
{/* امتیاز کاربران */}
|
||||||
<section className="bg-container rounded-container shadow-container p-4 py-6 grid grid-cols-2 items-center">
|
<section className={glassSurfaceCard('p-4 py-6 grid grid-cols-2 items-center')}>
|
||||||
<Skeleton className="h-16 w-20 rounded-md mx-auto" />
|
<Skeleton className="h-16 w-20 rounded-md mx-auto" />
|
||||||
<div className="flex flex-col w-fit items-end gap-1">
|
<div className="flex flex-col w-fit items-end gap-1">
|
||||||
{[...Array(5)].map((_, index) => (
|
{[...Array(5)].map((_, index) => (
|
||||||
@@ -87,7 +88,7 @@ const AboutSkeleton = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* نظرات کاربران */}
|
{/* نظرات کاربران */}
|
||||||
<section className="bg-container rounded-container shadow-container pt-3 pb-3.5 px-[16px] mt-4">
|
<section className={glassSurfaceCard('pt-3 pb-3.5 px-[16px] mt-4')}>
|
||||||
<div className="flex justify-between items-center border-b-[1.5px] border-border pb-2">
|
<div className="flex justify-between items-center border-b-[1.5px] border-border pb-2">
|
||||||
<Skeleton className="h-5 w-24 rounded-md" />
|
<Skeleton className="h-5 w-24 rounded-md" />
|
||||||
<Skeleton className="h-8 w-28 rounded-xl" />
|
<Skeleton className="h-8 w-28 rounded-xl" />
|
||||||
|
|||||||
@@ -1,17 +1,37 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import * as api from "../service/AboutService";
|
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import * as api from "../service/AboutService";
|
||||||
|
import {
|
||||||
|
getPersistedAboutData,
|
||||||
|
setPersistedAboutData,
|
||||||
|
} from "@/lib/helpers/themeCache";
|
||||||
|
|
||||||
export const useGetAbout = () => {
|
export const useGetAbout = () => {
|
||||||
const { name } = useParams<{ name: string }>();
|
const { name } = useParams<{ name: string }>();
|
||||||
return useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["about", name],
|
queryKey: ["about", name],
|
||||||
queryFn: () => api.getAbout(name),
|
queryFn: async () => {
|
||||||
|
const data = await api.getAbout(name);
|
||||||
|
setPersistedAboutData(name, data);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
enabled: !!name,
|
enabled: !!name,
|
||||||
retry: false,
|
retry: false,
|
||||||
staleTime: 5 * 60_000,
|
staleTime: 60 * 60_000,
|
||||||
refetchOnMount: false,
|
gcTime: 24 * 60 * 60_000,
|
||||||
|
refetchOnMount: "always",
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
placeholderData: () => (name ? getPersistedAboutData(name) : undefined),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (name && query.data) {
|
||||||
|
setPersistedAboutData(name, query.data);
|
||||||
|
}
|
||||||
|
}, [name, query.data]);
|
||||||
|
|
||||||
|
return query;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetReviews = () => {
|
export const useGetReviews = () => {
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ export interface Restaurant {
|
|||||||
vat: number;
|
vat: number;
|
||||||
domain: string;
|
domain: string;
|
||||||
score: Score;
|
score: Score;
|
||||||
plan: 'base' | 'premium';
|
plan: "base" | "premium";
|
||||||
|
bgType?: "pattern" | "custom" | "color";
|
||||||
|
bgBlur?: number;
|
||||||
|
bgOpacity?: number;
|
||||||
|
bgOverlay?: string;
|
||||||
|
bgUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReviewFood {
|
export interface ReviewFood {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import CartSkeleton from "./components/CartSkeleton";
|
|||||||
import CartSummary from "./components/CartSummary";
|
import CartSummary from "./components/CartSummary";
|
||||||
import { useCart } from "./hook/useCart";
|
import { useCart } from "./hook/useCart";
|
||||||
import { useCartPageState } from "./hooks/useCartPageState";
|
import { useCartPageState } from "./hooks/useCartPageState";
|
||||||
|
import { glassSurfaceCard } from "@/lib/styles/glassSurface";
|
||||||
|
|
||||||
const CartPage = () => {
|
const CartPage = () => {
|
||||||
const { t } = useTranslation("parallels", { keyPrefix: "Cart" });
|
const { t } = useTranslation("parallels", { keyPrefix: "Cart" });
|
||||||
@@ -42,7 +43,7 @@ const CartPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-y-auto h-full noscrollbar flex flex-col bg-background">
|
<div className="overflow-y-auto h-full noscrollbar flex flex-col">
|
||||||
<div className="grid grid-cols-3 items-center mt-6">
|
<div className="grid grid-cols-3 items-center mt-6">
|
||||||
{isCartEmpty || isLoading ? (
|
{isCartEmpty || isLoading ? (
|
||||||
<span></span>
|
<span></span>
|
||||||
@@ -93,7 +94,7 @@ const CartPage = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 pb-24">
|
<div className="flex flex-col gap-4 pb-24">
|
||||||
{!isPremium && (
|
{!isPremium && (
|
||||||
<div className="bg-container rounded-container p-4 border border-border shadow-sm">
|
<div className={glassSurfaceCard('p-4')}>
|
||||||
<div className="flex items-start gap-3 mb-4">
|
<div className="flex items-start gap-3 mb-4">
|
||||||
<div className="shrink-0 mt-0.5">
|
<div className="shrink-0 mt-0.5">
|
||||||
<NotificationBellIcon
|
<NotificationBellIcon
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import { useGetFoods } from "@/app/[name]/(Main)/hooks/useMenuData";
|
||||||
import Link from 'next/link';
|
import { useGetProfile } from "@/app/[name]/(Profile)/profile/hooks/userProfileData";
|
||||||
import { useParams, useRouter, usePathname } from 'next/navigation';
|
import Button from "@/components/button/PrimaryButton";
|
||||||
import Button from '@/components/button/PrimaryButton';
|
import { glassSurfaceNav } from "@/lib/styles/glassSurface";
|
||||||
import { useGetFoods } from '@/app/[name]/(Main)/hooks/useMenuData';
|
import Link from "next/link";
|
||||||
import { useCart } from '../hook/useCart';
|
import { useParams, usePathname, useRouter } from "next/navigation";
|
||||||
import { useTranslation } from 'react-i18next';
|
import React from "react";
|
||||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
import { useTranslation } from "react-i18next";
|
||||||
import { useGetCartItems } from '../hooks/useCartData';
|
import { useCart } from "../hook/useCart";
|
||||||
import { useCartStore } from '../store/Store';
|
import { useGetCartItems } from "../hooks/useCartData";
|
||||||
import { guestCartItemsToFoods } from '../lib/cartUtils';
|
import { guestCartItemsToFoods } from "../lib/cartUtils";
|
||||||
|
import { useCartStore } from "../store/Store";
|
||||||
|
|
||||||
interface CartSummaryProps {
|
interface CartSummaryProps {
|
||||||
isPremium: boolean;
|
isPremium: boolean;
|
||||||
@@ -26,7 +27,7 @@ const CartSummary = ({ isPremium, hasItems }: CartSummaryProps) => {
|
|||||||
const { data: foodsResponse } = useGetFoods();
|
const { data: foodsResponse } = useGetFoods();
|
||||||
const { items } = useCart();
|
const { items } = useCart();
|
||||||
const { data: cartData } = useGetCartItems(isSuccess);
|
const { data: cartData } = useGetCartItems(isSuccess);
|
||||||
const { t } = useTranslation('parallels', { keyPrefix: 'Cart' });
|
const { t } = useTranslation("parallels", { keyPrefix: "Cart" });
|
||||||
const { description, setDescription } = useCartStore();
|
const { description, setDescription } = useCartStore();
|
||||||
|
|
||||||
const totalPrice = React.useMemo(() => {
|
const totalPrice = React.useMemo(() => {
|
||||||
@@ -45,10 +46,7 @@ const CartSummary = ({ isPremium, hasItems }: CartSummaryProps) => {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}, [isSuccess, cartData?.data?.total, foodsResponse?.data, items]);
|
}, [isSuccess, cartData?.data?.total, foodsResponse?.data, items]);
|
||||||
|
|
||||||
const formatPrice = React.useCallback(
|
const formatPrice = React.useCallback((value: number) => value.toLocaleString("fa-IR"), []);
|
||||||
(value: number) => value.toLocaleString('fa-IR'),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasItems) {
|
if (!hasItems) {
|
||||||
return null;
|
return null;
|
||||||
@@ -57,36 +55,32 @@ const CartSummary = ({ isPremium, hasItems }: CartSummaryProps) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isPremium && (
|
{isPremium && (
|
||||||
<div className='mt-2 mb-4'>
|
<div className="mt-2 mb-4">
|
||||||
<div className='relative'>
|
<div className="relative">
|
||||||
<h3 className='text-sm'>{t('InputDescription.Label')}</h3>
|
<h3 className="text-sm">{t("InputDescription.Label")}</h3>
|
||||||
<textarea
|
<textarea
|
||||||
className='w-full px-4 py-2.5 mt-4 text-sm2 leading-6 outline-0 border border-border rounded-normal resize-none focus:ring-0'
|
className="w-full px-4 py-2.5 mt-4 text-sm2 leading-6 outline-0 border border-white rounded-normal bg-white/50 resize-none focus:ring-0"
|
||||||
placeholder={t('InputDescription.Placeholder')}
|
placeholder={t("InputDescription.Placeholder")}
|
||||||
id='description'
|
id="description"
|
||||||
name='description'
|
name="description"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
></textarea>
|
></textarea>
|
||||||
<span className='absolute top-4 right-2 px-2 bg-background text-foreground text-xs'></span>
|
<span className="absolute top-4 right-2 px-2 bg-background text-foreground text-xs"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-container border-t border-border p-4 w-full'>
|
<div className={glassSurfaceNav("fixed bottom-0 left-0 right-0 z-50 border-t border-border p-4 w-full")}>
|
||||||
<div className='flex justify-between items-center gap-4'>
|
<div className="flex justify-between items-center gap-4">
|
||||||
<div className='flex flex-col'>
|
<div className="flex flex-col">
|
||||||
<div className='text-xs text-gray-400 dark:text-disabled-text'>{t('PayableAmountLabel')}</div>
|
<div className="text-xs text-gray-400 dark:text-disabled-text">{t("PayableAmountLabel")}</div>
|
||||||
{isPremium && (
|
{isPremium && <div className="text-sm mt-2 font-semibold dark:text-foreground">{formatPrice(totalPrice)} تومان</div>}
|
||||||
<div className='text-sm mt-2 font-semibold dark:text-foreground'>
|
|
||||||
{formatPrice(totalPrice)} تومان
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isPremium && (
|
{isPremium && (
|
||||||
<Link
|
<Link
|
||||||
href={'order/checkout/0'}
|
href={"order/checkout/0"}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -96,16 +90,10 @@ const CartSummary = ({ isPremium, hasItems }: CartSummaryProps) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button className='w-fit! px-10 dark:bg-white dark:text-black! dark:hover:bg-gray-100'>
|
<Button className="w-fit! px-10 dark:bg-white dark:text-black! dark:hover:bg-gray-100">{t("ButtonCheckout")}</Button>
|
||||||
{t('ButtonCheckout')}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{!isPremium && (
|
{!isPremium && <div className="text-sm mt-2 font-semibold dark:text-foreground">{formatPrice(totalPrice)} تومان</div>}
|
||||||
<div className='text-sm mt-2 font-semibold dark:text-foreground'>
|
|
||||||
{formatPrice(totalPrice)} تومان
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ef } from '@/lib/helpers/utfNumbers'
|
|||||||
import { EmojiHappy, Microphone2, Paperclip2 } from 'iconsax-react'
|
import { EmojiHappy, Microphone2, Paperclip2 } from 'iconsax-react'
|
||||||
import React, { useMemo, useState } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface'
|
||||||
|
|
||||||
type Props = object
|
type Props = object
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ function ChatIndex ({}: Props) {
|
|||||||
<section className='flex flex-col h-full pt-6'>
|
<section className='flex flex-col h-full pt-6'>
|
||||||
<h1 className='font-medium'>علی مصلحی</h1>
|
<h1 className='font-medium'>علی مصلحی</h1>
|
||||||
|
|
||||||
<div className='bg-container rounded-[30px] flex flex-col justify-between mt-6 flex-1 px-6 pt-7 pb-8 overflow-hidden'>
|
<div className={glassSurfaceCard('rounded-[30px] flex flex-col justify-between mt-6 flex-1 px-6 pt-7 pb-8 overflow-hidden')}>
|
||||||
<div className='overflow-y-auto flex flex-col-reverse'>
|
<div className='overflow-y-auto flex flex-col-reverse'>
|
||||||
<ul className='grid gap-6 pb-6 '>
|
<ul className='grid gap-6 pb-6 '>
|
||||||
{messagesMemo.map((v, i) => {
|
{messagesMemo.map((v, i) => {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import useToggle from '@/hooks/helpers/useToggle'
|
import useToggle from '@/hooks/helpers/useToggle'
|
||||||
import Prompt from '@/components/utils/Prompt'
|
import Prompt from '@/components/utils/Prompt'
|
||||||
|
import { glassSurfaceFlat } from '@/lib/styles/glassSurface'
|
||||||
|
|
||||||
type Props = object
|
type Props = object
|
||||||
|
|
||||||
@@ -170,7 +171,7 @@ function ChatIndex ({}: Props) {
|
|||||||
{selectedChats.length > 0 ? (
|
{selectedChats.length > 0 ? (
|
||||||
<span
|
<span
|
||||||
onClick={toggleDeleteModal}
|
onClick={toggleDeleteModal}
|
||||||
className='bg-container p-2 rounded-xl active:bg-gray-50 dark:active:bg-neutral-500'
|
className={glassSurfaceFlat('p-2 rounded-xl active:brightness-95 cursor-pointer')}
|
||||||
>
|
>
|
||||||
<Trash
|
<Trash
|
||||||
size={24}
|
size={24}
|
||||||
@@ -180,7 +181,7 @@ function ChatIndex ({}: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href={'chat/nearby'}
|
href={'chat/nearby'}
|
||||||
className='bg-container p-2 rounded-xl active:bg-gray-50 dark:active:bg-neutral-500'
|
className={glassSurfaceFlat('p-2 rounded-xl active:brightness-95 cursor-pointer')}
|
||||||
>
|
>
|
||||||
<Radar2
|
<Radar2
|
||||||
size={24}
|
size={24}
|
||||||
@@ -198,7 +199,7 @@ function ChatIndex ({}: Props) {
|
|||||||
<li
|
<li
|
||||||
key={i}
|
key={i}
|
||||||
{...longPress(chat.id)}
|
{...longPress(chat.id)}
|
||||||
className='select-none flex items-center gap-x-2 hover:brightness-105 cursor-default active:brightness-102 p-2 bg-background rounded-xl'
|
className={glassSurfaceFlat('select-none flex items-center gap-x-2 hover:brightness-105 cursor-default active:brightness-102 p-2 rounded-xl')}
|
||||||
>
|
>
|
||||||
<div className='size-[49px] min-w-[49px]'>
|
<div className='size-[49px] min-w-[49px]'>
|
||||||
{selectedChats.includes(chat.id) ? (
|
{selectedChats.includes(chat.id) ? (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import CategorySmallItemRenderer from "@/components/listview/CategorySmallItemRe
|
|||||||
import HorizontalScrollView from "@/components/listview/HorizontalScrollView";
|
import HorizontalScrollView from "@/components/listview/HorizontalScrollView";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
import { GLASS_SURFACE_SELECTED } from "@/lib/styles/glassSurface";
|
||||||
|
|
||||||
/** TODO: remove when backend sends avatarRenderMode */
|
/** TODO: remove when backend sends avatarRenderMode */
|
||||||
const COLORED_SVG_RESTAURANT_SLUGS = new Set(["havasone"]);
|
const COLORED_SVG_RESTAURANT_SLUGS = new Set(["havasone"]);
|
||||||
@@ -110,7 +111,7 @@ const CategoryScroll = ({ categories, selectedCategory, onSelect, variant = "lar
|
|||||||
const isSelected = item.id === selectedCategory;
|
const isSelected = item.id === selectedCategory;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Renderer key={item.id} className={clsx(isSelected && "bg-container!")} onClick={handleSelect(item.id)}>
|
<Renderer key={item.id} className={clsx(isSelected && GLASS_SURFACE_SELECTED)} onClick={handleSelect(item.id)}>
|
||||||
<CategoryImage src={item.avatarUrl || "/assets/images/food-image.png"} size={imageSize} alt="category image" tintWithPrimary={!usesColoredSvg} />
|
<CategoryImage src={item.avatarUrl || "/assets/images/food-image.png"} size={imageSize} alt="category image" tintWithPrimary={!usesColoredSvg} />
|
||||||
<span className="text-xs text-foreground text-center whitespace-nowrap">{item.title}</span>
|
<span className="text-xs text-foreground text-center whitespace-nowrap">{item.title}</span>
|
||||||
</Renderer>
|
</Renderer>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import useToggle from "@/hooks/helpers/useToggle";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { Candle2 } from "iconsax-react";
|
import { Candle2 } from "iconsax-react";
|
||||||
|
import { glassSurfaceFlat } from "@/lib/styles/glassSurface";
|
||||||
import { useQueryState } from "next-usequerystate";
|
import { useQueryState } from "next-usequerystate";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -21,12 +22,12 @@ import type { Category, Food } from "../types/Types";
|
|||||||
const sortings = ["PopularityDescending", "RateDescending", "PriceAscending", "PriceDescending"] as const;
|
const sortings = ["PopularityDescending", "RateDescending", "PriceAscending", "PriceDescending"] as const;
|
||||||
|
|
||||||
const MenuIndex = () => {
|
const MenuIndex = () => {
|
||||||
const { data: foodsData, isLoading: foodsLoading } = useGetFoods();
|
const { data: foodsData, isPending: foodsPending } = useGetFoods();
|
||||||
const { data: categoriesData, isLoading: categoriesLoading } = useGetCategories();
|
const { data: categoriesData, isPending: categoriesPending } = useGetCategories();
|
||||||
const foods = useMemo<Food[]>(() => foodsData?.data || [], [foodsData?.data]);
|
const foods = useMemo<Food[]>(() => foodsData?.data || [], [foodsData?.data]);
|
||||||
const categories = useMemo<Category[]>(() => categoriesData?.data || [], [categoriesData?.data]);
|
const categories = useMemo<Category[]>(() => categoriesData?.data || [], [categoriesData?.data]);
|
||||||
|
|
||||||
const isLoading = foodsLoading || categoriesLoading;
|
const isLoading = foodsPending || categoriesPending;
|
||||||
const { t: tCommon } = useTranslation("common");
|
const { t: tCommon } = useTranslation("common");
|
||||||
const { t: tMenu } = useTranslation("menu", { keyPrefix: "Menu" });
|
const { t: tMenu } = useTranslation("menu", { keyPrefix: "Menu" });
|
||||||
const { state: filterModal, toggle: toggleFilterModal } = useToggle();
|
const { state: filterModal, toggle: toggleFilterModal } = useToggle();
|
||||||
@@ -175,11 +176,11 @@ const MenuIndex = () => {
|
|||||||
<div className="flex flex-wrap gap-2 items-center relative" ref={smallCategoriesRef}>
|
<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>
|
<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">
|
<div className="flex min-w-[247px] flex-1 gap-2 justify-end items-center">
|
||||||
<button onClick={toggleFilterModal} className="rounded-xl h-8 bg-container ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]">
|
<button onClick={toggleFilterModal} className={glassSurfaceFlat('rounded-xl h-8 ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]')}>
|
||||||
<Candle2 className="stroke-foreground" size={16} />
|
<Candle2 className="stroke-foreground" size={16} />
|
||||||
<span className="text-xs leading-5 font-medium whitespace-nowrap">{tMenu("MenuFilterDrawer.Label")}</span>
|
<span className="text-xs leading-5 font-medium whitespace-nowrap">{tMenu("MenuFilterDrawer.Label")}</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={toggleSortingModal} className="rounded-xl h-8 bg-container ps-4 pe-2 py-1.5 inline-flex items-center gap-2">
|
<button onClick={toggleSortingModal} className={glassSurfaceFlat('rounded-xl h-8 ps-4 pe-2 py-1.5 inline-flex items-center gap-2')}>
|
||||||
<TextAlignIcon className="text-foreground" />
|
<TextAlignIcon className="text-foreground" />
|
||||||
<span className="text-xs leading-5 font-medium whitespace-nowrap">{activeSortingLabel}</span>
|
<span className="text-xs leading-5 font-medium whitespace-nowrap">{activeSortingLabel}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -196,7 +197,7 @@ const MenuIndex = () => {
|
|||||||
opacity: smallCategoriesVisible ? 1 : 0,
|
opacity: smallCategoriesVisible ? 1 : 0,
|
||||||
}}
|
}}
|
||||||
transition={{ duration: 0.1 }}
|
transition={{ duration: 0.1 }}
|
||||||
className={clsx("fixed left-0 z-10 top-0 px-4 pt-16 bg-[#F4F5F9CC] dark:bg-background/70 backdrop-blur-[44px] right-0 xl:pr-72 xl:pt-20", !smallCategoriesVisible && "pointer-events-none")}
|
className={clsx(glassSurfaceFlat('fixed left-0 z-10 top-0 px-4 pt-16 right-0 xl:pr-72 xl:pt-20 border-x-0 border-t-0 rounded-none'), !smallCategoriesVisible && "pointer-events-none")}
|
||||||
>
|
>
|
||||||
<CategoryScroll categories={categories} selectedCategory={selectedCategory} onSelect={updateCategory} variant="small" />
|
<CategoryScroll categories={categories} selectedCategory={selectedCategory} onSelect={updateCategory} variant="small" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -1,28 +1,63 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useEffect } from "react";
|
||||||
import * as api from "../service/MenuService";
|
import * as api from "../service/MenuService";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { hasAuthToken } from "@/lib/api/func";
|
import { hasAuthToken } from "@/lib/api/func";
|
||||||
|
import {
|
||||||
|
getPersistedCategoriesData,
|
||||||
|
getPersistedMenuData,
|
||||||
|
setPersistedCategoriesData,
|
||||||
|
setPersistedMenuData,
|
||||||
|
} from "@/lib/helpers/themeCache";
|
||||||
|
|
||||||
export const useGetFoods = () => {
|
export const useGetFoods = () => {
|
||||||
const { name } = useParams<{ name: string }>();
|
const { name } = useParams<{ name: string }>();
|
||||||
return useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["menu", name],
|
queryKey: ["menu", name],
|
||||||
queryFn: () => api.getFoods(name),
|
queryFn: async () => {
|
||||||
|
const data = await api.getFoods(name);
|
||||||
|
setPersistedMenuData(name, data);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
enabled: !!name,
|
enabled: !!name,
|
||||||
staleTime: 5 * 60_000,
|
staleTime: 5 * 60_000,
|
||||||
refetchOnMount: false,
|
refetchOnMount: "always",
|
||||||
|
placeholderData: () => (name ? getPersistedMenuData(name) : undefined),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (name && query.data) {
|
||||||
|
setPersistedMenuData(name, query.data);
|
||||||
|
}
|
||||||
|
}, [name, query.data]);
|
||||||
|
|
||||||
|
return query;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetCategories = () => {
|
export const useGetCategories = () => {
|
||||||
const { name } = useParams<{ name: string }>();
|
const { name } = useParams<{ name: string }>();
|
||||||
return useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["categories", name],
|
queryKey: ["categories", name],
|
||||||
queryFn: () => api.getCategories(name),
|
queryFn: async () => {
|
||||||
|
const data = await api.getCategories(name);
|
||||||
|
setPersistedCategoriesData(name, data);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
enabled: !!name,
|
enabled: !!name,
|
||||||
staleTime: 5 * 60_000,
|
staleTime: 5 * 60_000,
|
||||||
refetchOnMount: false,
|
refetchOnMount: "always",
|
||||||
|
placeholderData: () => (name ? getPersistedCategoriesData(name) : undefined),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (name && query.data) {
|
||||||
|
setPersistedCategoriesData(name, query.data);
|
||||||
|
}
|
||||||
|
}, [name, query.data]);
|
||||||
|
|
||||||
|
return query;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetNotificationsCount = () => {
|
export const useGetNotificationsCount = () => {
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import LoadingOverlay from '@/components/overlays/LoadingOverlay'
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
function Loading() {
|
|
||||||
return (
|
|
||||||
<LoadingOverlay />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Loading
|
|
||||||
@@ -13,6 +13,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useGetOrders } from './hooks/useHistoryData';
|
import { useGetOrders } from './hooks/useHistoryData';
|
||||||
import { HistoryOrderItem } from './types/Types';
|
import { HistoryOrderItem } from './types/Types';
|
||||||
import { OrderStatus } from './enum/Enum';
|
import { OrderStatus } from './enum/Enum';
|
||||||
|
import { glassSurface } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
const fallbackImage = '/assets/images/food-preview.png';
|
const fallbackImage = '/assets/images/food-preview.png';
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ function OrdersIndex() {
|
|||||||
const items: HistoryOrderItem[] = order.items || [];
|
const items: HistoryOrderItem[] = order.items || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={order.id} className='w-full px-[17px] mb-4 pt-8 pb-6 bg-container rounded-3xl'>
|
<div key={order.id} className={glassSurface('w-full px-[17px] mb-4 pt-8 pb-6 rounded-3xl')}>
|
||||||
<div className="text-sm2">
|
<div className="text-sm2">
|
||||||
<div className='flex items-center justify-between mb-2'>
|
<div className='flex items-center justify-between mb-2'>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center'>
|
||||||
@@ -165,7 +166,7 @@ function OrdersIndex() {
|
|||||||
const items: HistoryOrderItem[] = order.items || [];
|
const items: HistoryOrderItem[] = order.items || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={order.id} className='w-full px-[17px] mb-4 pt-8 pb-6 bg-container rounded-3xl'>
|
<div key={order.id} className={glassSurface('w-full px-[17px] mb-4 pt-8 pb-6 rounded-3xl')}>
|
||||||
<div className="text-sm2">
|
<div className="text-sm2">
|
||||||
<div className='flex items-center justify-between mb-2'>
|
<div className='flex items-center justify-between mb-2'>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center'>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { CreateRateType } from '../types/Types';
|
|||||||
import { toast } from '@/components/Toast';
|
import { toast } from '@/components/Toast';
|
||||||
import { PositivePointEnum, NegativePointEnum } from '../enum/Enum';
|
import { PositivePointEnum, NegativePointEnum } from '../enum/Enum';
|
||||||
import { extractErrorMessage } from '@/lib/func';
|
import { extractErrorMessage } from '@/lib/func';
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
type Props = object
|
type Props = object
|
||||||
|
|
||||||
@@ -148,7 +149,7 @@ function RatingOrderIndex({ }: Props) {
|
|||||||
<section className='flex flex-col h-full pt-6'>
|
<section className='flex flex-col h-full pt-6'>
|
||||||
<h1 className='font-medium'>{t("Heading")}</h1>
|
<h1 className='font-medium'>{t("Heading")}</h1>
|
||||||
|
|
||||||
<form onSubmit={submitForm} className='bg-container flex flex-col h-min justify-between rounded-[30px] pt-10 px-6 pb-7.5 mt-4'>
|
<form onSubmit={submitForm} className={glassSurfaceCard('flex flex-col h-min justify-between rounded-[30px] pt-10 px-6 pb-7.5 mt-4')}>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { useGetAbout } from '../../../about/hooks/useAboutData';
|
import { useGetAbout } from '../../../about/hooks/useAboutData';
|
||||||
import { toast } from '@/components/Toast';
|
import { toast } from '@/components/Toast';
|
||||||
import { extractErrorMessage } from '@/lib/func';
|
import { extractErrorMessage } from '@/lib/func';
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
function TransactionsIndex() {
|
function TransactionsIndex() {
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ function TransactionsIndex() {
|
|||||||
|
|
||||||
<section
|
<section
|
||||||
aria-label="درباره باشگاه مشتریان"
|
aria-label="درباره باشگاه مشتریان"
|
||||||
className='mt-3 bg-container dark:bg-container rounded-normal grid grid-cols-4 items-center border border-transparent dark:border-border/50'
|
className={glassSurfaceCard('mt-3 rounded-normal grid grid-cols-4 items-center')}
|
||||||
>
|
>
|
||||||
<div className='col-span-3 py-5 pe-7 ps-5 relative h-36.5'>
|
<div className='col-span-3 py-5 pe-7 ps-5 relative h-36.5'>
|
||||||
<div className='w-full h-full absolute top-0 left-0 overflow-clip text-muted-foreground/30 dark:text-foreground/20'>
|
<div className='w-full h-full absolute top-0 left-0 overflow-clip text-muted-foreground/30 dark:text-foreground/20'>
|
||||||
@@ -78,7 +79,7 @@ function TransactionsIndex() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* بخش نمایش اعتبار کاربر */}
|
{/* بخش نمایش اعتبار کاربر */}
|
||||||
<section className='mt-6 bg-container dark:bg-container rounded-normal p-5 border border-transparent dark:border-border/50'>
|
<section className={glassSurfaceCard('mt-6 rounded-normal p-5')}>
|
||||||
<div className='flex items-center gap-2 mb-4'>
|
<div className='flex items-center gap-2 mb-4'>
|
||||||
<MoneyRecive color='currentColor' size={24} className='text-primary dark:text-foreground' variant='Bold' />
|
<MoneyRecive color='currentColor' size={24} className='text-primary dark:text-foreground' variant='Bold' />
|
||||||
<h2 className='font-bold text-base dark:text-foreground'>اعتبار شما</h2>
|
<h2 className='font-bold text-base dark:text-foreground'>اعتبار شما</h2>
|
||||||
@@ -92,7 +93,7 @@ function TransactionsIndex() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* بخش تبدیل امتیاز به اعتبار */}
|
{/* بخش تبدیل امتیاز به اعتبار */}
|
||||||
<section className='mt-6 bg-container dark:bg-container rounded-normal p-5 border border-transparent dark:border-border/50'>
|
<section className={glassSurfaceCard('mt-6 rounded-normal p-5')}>
|
||||||
<div className='flex items-center gap-2 mb-4'>
|
<div className='flex items-center gap-2 mb-4'>
|
||||||
<MoneyRecive color='currentColor' size={24} className='text-primary dark:text-foreground' variant='Bold' />
|
<MoneyRecive color='currentColor' size={24} className='text-primary dark:text-foreground' variant='Bold' />
|
||||||
<h2 className='font-bold text-base dark:text-foreground'>تبدیل امتیاز به اعتبار</h2>
|
<h2 className='font-bold text-base dark:text-foreground'>تبدیل امتیاز به اعتبار</h2>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { useCallback } from 'react'
|
|||||||
import { useGetMyCoupons } from '../hooks/useTransactionData';
|
import { useGetMyCoupons } from '../hooks/useTransactionData';
|
||||||
import { toast } from '@/components/Toast';
|
import { toast } from '@/components/Toast';
|
||||||
import type { Coupon } from '../types/Types';
|
import type { Coupon } from '../types/Types';
|
||||||
|
import { glassSurfaceCard, glassSurfaceCardFlat } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
function TransactionsIndex() {
|
function TransactionsIndex() {
|
||||||
const { data: myCoupons, isLoading } = useGetMyCoupons();
|
const { data: myCoupons, isLoading } = useGetMyCoupons();
|
||||||
@@ -32,7 +33,7 @@ function TransactionsIndex() {
|
|||||||
|
|
||||||
<section
|
<section
|
||||||
aria-label="درباره کدهای تخفیف"
|
aria-label="درباره کدهای تخفیف"
|
||||||
className='mt-3 bg-container rounded-normal grid grid-cols-4 items-center'
|
className={glassSurfaceCard('mt-3 rounded-normal grid grid-cols-4 items-center')}
|
||||||
>
|
>
|
||||||
<div className='col-span-3 py-9.5 pe-7 ps-5 relative'>
|
<div className='col-span-3 py-9.5 pe-7 ps-5 relative'>
|
||||||
<div className='w-full h-full absolute top-0 left-0 overflow-clip'>
|
<div className='w-full h-full absolute top-0 left-0 overflow-clip'>
|
||||||
@@ -94,7 +95,7 @@ function TransactionsIndex() {
|
|||||||
{coupons.map((coupon) => (
|
{coupons.map((coupon) => (
|
||||||
<article
|
<article
|
||||||
key={coupon.id}
|
key={coupon.id}
|
||||||
className='bg-container rounded-normal flex justify-between items-center'
|
className={glassSurfaceCardFlat('rounded-normal flex justify-between items-center')}
|
||||||
>
|
>
|
||||||
<div className='py-4 pe-7 ps-5 border-e-2 w-full border-border border-dashed'>
|
<div className='py-4 pe-7 ps-5 border-e-2 w-full border-border border-dashed'>
|
||||||
<h3 className='font-bold text-sm2'>مبلغ تخفیف: {formatDiscount(coupon)}</h3>
|
<h3 className='font-bold text-sm2'>مبلغ تخفیف: {formatDiscount(coupon)}</h3>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React from 'react'
|
|||||||
import { useGetTransactions } from './hooks/useTransactionData';
|
import { useGetTransactions } from './hooks/useTransactionData';
|
||||||
import { TransactionsTable } from './components/TransactionsTable';
|
import { TransactionsTable } from './components/TransactionsTable';
|
||||||
import LoadingOverlay from '@/components/overlays/LoadingOverlay';
|
import LoadingOverlay from '@/components/overlays/LoadingOverlay';
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
function TransactionsReviewIndex() {
|
function TransactionsReviewIndex() {
|
||||||
const { data: transactionsResponse, isLoading: isLoadingTransactions } = useGetTransactions();
|
const { data: transactionsResponse, isLoading: isLoadingTransactions } = useGetTransactions();
|
||||||
@@ -56,7 +57,7 @@ function TransactionsReviewIndex() {
|
|||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
<div className='w-full h-full bg-container rounded-3xl mt-0'>
|
<div className={glassSurfaceCard('w-full h-full mt-0 rounded-3xl')}>
|
||||||
<TransactionsTable transactions={transactions} />
|
<TransactionsTable transactions={transactions} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const AddressDetailsModal = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onChangePosition();
|
onChangePosition();
|
||||||
}}
|
}}
|
||||||
className='bg-background! w-fit! text-foreground! flex items-center gap-2 text-xs'
|
className='bg-background! pattern-secondary-bg w-fit! text-foreground! flex items-center gap-2 text-xs'
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
<Location color='#6b7280' variant='Bold' className='size-4' />
|
<Location color='#6b7280' variant='Bold' className='size-4' />
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Address } from './types/Types';
|
|||||||
import Prompt from '@/components/utils/Prompt';
|
import Prompt from '@/components/utils/Prompt';
|
||||||
import { toast } from '@/components/Toast';
|
import { toast } from '@/components/Toast';
|
||||||
import useToggle from '@/hooks/helpers/useToggle';
|
import useToggle from '@/hooks/helpers/useToggle';
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
type Props = object
|
type Props = object
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ function UserAddressesPage({ }: Props) {
|
|||||||
addresses.map((address) => (
|
addresses.map((address) => (
|
||||||
<div
|
<div
|
||||||
key={address.id}
|
key={address.id}
|
||||||
className={`bg-container rounded-container w-full shadow-xl px-4 py-4 flex flex-col justify-between ${address.isDefault ? 'border border-primary' : ''}`}
|
className={glassSurfaceCard(`w-full px-4 py-4 flex flex-col justify-between ${address.isDefault ? 'border border-primary' : ''}`)}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-start mb-2">
|
<div className="flex justify-between items-start mb-2">
|
||||||
<h2 className='text-sm font-medium'>{address.title}</h2>
|
<h2 className='text-sm font-medium'>{address.title}</h2>
|
||||||
@@ -132,7 +133,7 @@ function UserAddressesPage({ }: Props) {
|
|||||||
onClick={() => handleSetDefault(address.id)}
|
onClick={() => handleSetDefault(address.id)}
|
||||||
disabled={isSettingDefault}
|
disabled={isSettingDefault}
|
||||||
pending={isSettingDefault}
|
pending={isSettingDefault}
|
||||||
className='bg-background! text-foreground! flex items-center gap-2 text-xs h-8 flex-1'
|
className='bg-background! pattern-secondary-bg text-foreground! flex items-center gap-2 text-xs h-8 flex-1'
|
||||||
>
|
>
|
||||||
<TickCircle variant='Bold' className='fill-foreground stroke-background size-4.5 mb-0.5' />
|
<TickCircle variant='Bold' className='fill-foreground stroke-background size-4.5 mb-0.5' />
|
||||||
تنظیم به عنوان فعال
|
تنظیم به عنوان فعال
|
||||||
@@ -140,14 +141,14 @@ function UserAddressesPage({ }: Props) {
|
|||||||
)}
|
)}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Link href={getEditHref(address.id)}>
|
<Link href={getEditHref(address.id)}>
|
||||||
<Button className='bg-background! text-foreground! flex justify-center items-center gap-2 text-xs p-0! size-8!'>
|
<Button className='bg-background! pattern-secondary-bg text-foreground! flex justify-center items-center gap-2 text-xs p-0! size-8!'>
|
||||||
<Edit2 className='stroke-foreground size-5 mb-0.5' />
|
<Edit2 className='stroke-foreground size-5 mb-0.5' />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleDeleteClick(address.id)}
|
onClick={() => handleDeleteClick(address.id)}
|
||||||
disabled={isDeleting}
|
disabled={isDeleting}
|
||||||
className='bg-background! text-foreground! flex justify-center items-center gap-2 text-xs p-0! size-8!'
|
className='bg-background! pattern-secondary-bg text-foreground! flex justify-center items-center gap-2 text-xs p-0! size-8!'
|
||||||
>
|
>
|
||||||
<Trash className='stroke-foreground size-5 mb-0.5' />
|
<Trash className='stroke-foreground size-5 mb-0.5' />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import Calendar from '@/components/ui/calendar';
|
|||||||
import { getDateLib } from "react-day-picker/persian";
|
import { getDateLib } from "react-day-picker/persian";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useGetProfile, useSingleUpload, useUpdateProfile } from '../hooks/userProfileData';
|
import { useGetProfile, useSingleUpload, useUpdateProfile } from '../hooks/userProfileData';
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { UpdateProfileType } from '../types/Types';
|
import { UpdateProfileType } from '../types/Types';
|
||||||
@@ -197,7 +198,7 @@ function ProfileIndex({ }: Props) {
|
|||||||
<h1 className='text-sm2 place-self-center font-medium'>ویرایش اطلاعات</h1>
|
<h1 className='text-sm2 place-self-center font-medium'>ویرایش اطلاعات</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={formik.handleSubmit} className="mt-8 flex-1 bg-container rounded-container w-full box-shadow-normal py-6 px-4 flex flex-col justify-between">
|
<form onSubmit={formik.handleSubmit} className={glassSurfaceCard('mt-8 flex-1 w-full py-6 px-4 flex flex-col justify-between')}>
|
||||||
<div className="bg-inherit">
|
<div className="bg-inherit">
|
||||||
<div className="justify-self-center w-fit relative text-center pb-10">
|
<div className="justify-self-center w-fit relative text-center pb-10">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Link from 'next/link';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useGetProfile } from './hooks/userProfileData';
|
import { useGetProfile } from './hooks/userProfileData';
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
type Props = object
|
type Props = object
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ function ProfileIndex({ }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flex-1 h-full bg-container rounded-container w-full box-shadow-normal py-6 px-4 flex flex-col justify-between">
|
<div className={glassSurfaceCard('mt-8 flex-1 h-full w-full py-6 px-4 flex flex-col justify-between')}>
|
||||||
|
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="flex items-center justify-start gap-3 pb-6 border-b-[1.5px] border-border">
|
<div className="flex items-center justify-start gap-3 pb-6 border-b-[1.5px] border-border">
|
||||||
|
|||||||
@@ -1,259 +1,198 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import Button from '@/components/button/PrimaryButton';
|
import Button from "@/components/button/PrimaryButton";
|
||||||
import PasswordField from '@/components/input/PasswordField';
|
import PasswordField from "@/components/input/PasswordField";
|
||||||
import TabContainer from '@/components/tab/TabContainer'
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { TabHeader } from '@/components/tab/TabHeader'
|
import Accordion from "@/components/utils/Accordion";
|
||||||
import { Switch } from '@/components/ui/switch';
|
import Seperator from "@/components/utils/Seperator";
|
||||||
import Accordion from '@/components/utils/Accordion';
|
import { SmartIcon } from "@/components/utils/SmartIcon";
|
||||||
import Seperator from '@/components/utils/Seperator';
|
import { getI18nObject } from "@/lib/i18n";
|
||||||
import { SmartIcon } from '@/components/utils/SmartIcon';
|
import { glassSurfaceCard } from "@/lib/styles/glassSurface";
|
||||||
import { getI18nObject } from '@/lib/i18n';
|
import { SettingsData } from "@/types/i18n/settings";
|
||||||
import { SettingsData } from '@/types/i18n/settings';
|
import { ArrowLeft, Key, TickCircle } from "iconsax-react";
|
||||||
import { Key, KeySquare, Notification1, ArrowLeft, TickCircle } from 'iconsax-react'
|
import { useRouter } from "next/navigation";
|
||||||
import { useRouter } from 'next/navigation';
|
import React, { Fragment, useState } from "react";
|
||||||
import React, { Fragment, useState } from 'react'
|
import { useTranslation } from "react-i18next";
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
function UserSettingsIndex() {
|
function UserSettingsIndex() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation('settings');
|
const { t } = useTranslation("settings");
|
||||||
const data: SettingsData = getI18nObject(t, "data");
|
const data: SettingsData = getI18nObject(t, "data");
|
||||||
const [statisticalParticipation, setStatisticalParticipation] = useState(true);
|
const [statisticalParticipation, setStatisticalParticipation] = useState(true);
|
||||||
|
|
||||||
const onSwitched = (e: React.MouseEvent) => {
|
const onSwitched = (e: React.MouseEvent) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
console.log('Save changed setting?', target.id);
|
console.log("Save changed setting?", target.id);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleStatisticalToggle = (checked: boolean) => {
|
const handleStatisticalToggle = (checked: boolean) => {
|
||||||
setStatisticalParticipation(checked);
|
setStatisticalParticipation(checked);
|
||||||
console.log('Statistical participation:', checked);
|
console.log("Statistical participation:", checked);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const notifsTab = () => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section aria-labelledby="about-title" className='py-4 flex-1 h-full'>
|
|
||||||
<section
|
|
||||||
className="bg-container rounded-container shadow-container p-4">
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center pb-[25px]">
|
|
||||||
<h2 className='text-sm leading-5'>{data.TabNotifications.Heading}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='p-2 pt-0'>
|
|
||||||
{data.TabNotifications.Accordions.map((accordion, i) => {
|
|
||||||
return (
|
|
||||||
<Fragment key={i}>
|
|
||||||
<Accordion
|
|
||||||
title={accordion.Heading}
|
|
||||||
group={'accordions'}
|
|
||||||
icon={accordion.Icon}
|
|
||||||
>
|
|
||||||
<div className='grid gap-4 mb-3'>
|
|
||||||
{accordion.Items.map((item, j) => {
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
key={j}
|
|
||||||
htmlFor={item.HtmlName}
|
|
||||||
className="flex w-full h-11 items-center justify-between gap-2 px-3 py-4 relative bg-container/29 rounded-xl border border-solid border-neutral-200 dark:border-neutral-600 focus:outline-none focus:ring-2 focus:ring-black"
|
|
||||||
>
|
|
||||||
|
|
||||||
<span className="inline-flex items-center gap-2.5 text-sm2">
|
|
||||||
<SmartIcon icon={item.Icon} />
|
|
||||||
{item.Label}
|
|
||||||
</span>
|
|
||||||
<Switch
|
|
||||||
onClick={onSwitched}
|
|
||||||
id={item.HtmlName}
|
|
||||||
name={item.HtmlName}
|
|
||||||
className="w-12 h-6 scale-[0.9]"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Accordion>
|
|
||||||
<Seperator className='last:hidden' />
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const passwordTab = () => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section aria-labelledby="reviews-title" className='py-4'>
|
|
||||||
<section
|
|
||||||
className="bg-container rounded-container shadow-container p-4">
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center pb-[25px]">
|
|
||||||
<h2 className='text-sm leading-5'>{data.TabPassword.Heading}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => e.preventDefault()}
|
|
||||||
className='grid gap-6 p-2'
|
|
||||||
>
|
|
||||||
{data.TabPassword.Inputs.map((input, i) => {
|
|
||||||
return (
|
|
||||||
<PasswordField
|
|
||||||
labelText={input.Label}
|
|
||||||
className='bg-container'
|
|
||||||
key={i}
|
|
||||||
htmlFor={input.HtmlName ?? ''}
|
|
||||||
placeholder={input.Placeholder}
|
|
||||||
autoComplete={input.AutoComplete}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
{data.TabPassword.Rules.map((rule, i) => {
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
key={i}
|
|
||||||
className='bg-background rounded-normal p-4 text-xs flex flex-col gap-2 font-light'
|
|
||||||
>
|
|
||||||
<h6 className='text-sm mb-2'>{rule.Heading}</h6>
|
|
||||||
{rule.Items.map((item, i) => {
|
|
||||||
return (
|
|
||||||
<li key={i}>{item}</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
<Button className='mt-4'>
|
|
||||||
<div className='flex items-center justify-center gap-1'>
|
|
||||||
<TickCircle className='stroke-white' size={20} />
|
|
||||||
<span className='mt-0.5'>
|
|
||||||
{data.TabPassword.ButtonSubmit}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const authenticatorTab = () => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section aria-labelledby="reviews-title" className='py-4'>
|
|
||||||
<div
|
|
||||||
className="bg-container rounded-container shadow-container p-4">
|
|
||||||
|
|
||||||
<div className="pb-4">
|
|
||||||
<h2 className='text-sm leading-5'>{data.TabAuthenticator.Heading}</h2>
|
|
||||||
<p className='text-xs mt-3 text-disabled-text font-light'>{data.TabAuthenticator.Description}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Seperator />
|
|
||||||
|
|
||||||
<div className='p-2'>
|
|
||||||
<div className='mt-14 bg-blue-100 p-4 place-self-center rounded-full'>
|
|
||||||
<Key size={32} className='stroke-blue-500' />
|
|
||||||
</div>
|
|
||||||
<p className='text-sm2 mt-4 font-light place-self-center'>
|
|
||||||
{data.TabAuthenticator.Tip}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className='place-self-center mt-2'>
|
|
||||||
<Button className='mt-4 px-14 py-2'>
|
|
||||||
<div className='flex items-center justify-center gap-1'>
|
|
||||||
<TickCircle className='stroke-white' size={20} />
|
|
||||||
<span className='mt-0.5'>
|
|
||||||
{data.TabAuthenticator.ButtonSubmit}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{data.TabAuthenticator.Guides.map((guide, i) => {
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
key={i}
|
|
||||||
className='bg-background rounded-normal mt-14 p-4 text-xs flex flex-col gap-2 font-light'
|
|
||||||
>
|
|
||||||
<h6 className='text-sm'>{guide.Heading}</h6>
|
|
||||||
<p className='text-disabled-text'>{guide.Description}</p>
|
|
||||||
{guide.Items.map((item, i) => {
|
|
||||||
return (
|
|
||||||
<div key={i}>
|
|
||||||
<div className='ml-2 mt-2 bg-blue-100 inline-block size-6 text-center pt-[5px] pr-[1px] text-blue-500 rounded-full'>
|
|
||||||
{item.Mark}
|
|
||||||
</div>
|
|
||||||
<span>
|
|
||||||
{item.Label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{guide.Notes.map((note, i) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className='mt-4 p-3 text-yellow-800 rounded-lg bg-yellow-50 border border-solid border-yellow-200'
|
|
||||||
>
|
|
||||||
{note}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const notifsTab = () => {
|
||||||
return (
|
return (
|
||||||
<div className='overflow-y-auto h-full noscrollbar flex flex-col gap-6'>
|
<section aria-labelledby="about-title" className="py-4 flex-1 h-full">
|
||||||
<div className='grid grid-cols-3 items-center'>
|
<section className={glassSurfaceCard("p-4")}>
|
||||||
<span></span>
|
<div className="flex justify-between items-center pb-[25px]">
|
||||||
<h1 className='text-sm2 place-self-center font-medium'>{data.Heading}</h1>
|
<h2 className="text-sm leading-5">{data.TabNotifications.Heading}</h2>
|
||||||
<ArrowLeft
|
</div>
|
||||||
className='cursor-pointer place-self-end'
|
|
||||||
size='24'
|
<div className="p-2 pt-0">
|
||||||
color='currentColor'
|
{data.TabNotifications.Accordions.map((accordion, i) => {
|
||||||
onClick={() => { router.back() }}
|
return (
|
||||||
/>
|
<Fragment key={i}>
|
||||||
|
<Accordion title={accordion.Heading} group={"accordions"} icon={accordion.Icon}>
|
||||||
|
<div className="grid gap-4 mb-3">
|
||||||
|
{accordion.Items.map((item, j) => {
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={j}
|
||||||
|
htmlFor={item.HtmlName}
|
||||||
|
className="flex w-full h-11 items-center justify-between gap-2 px-3 py-4 relative bg-container/29 rounded-xl border border-solid border-neutral-200 dark:border-neutral-600 focus:outline-none focus:ring-2 focus:ring-black"
|
||||||
|
>
|
||||||
|
<span className="inline-flex items-center gap-2.5 text-sm2">
|
||||||
|
<SmartIcon icon={item.Icon} />
|
||||||
|
{item.Label}
|
||||||
|
</span>
|
||||||
|
<Switch onClick={onSwitched} id={item.HtmlName} name={item.HtmlName} className="w-12 h-6 scale-[0.9]" />
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Accordion>
|
||||||
|
<Seperator className="last:hidden" />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const passwordTab = () => {
|
||||||
|
return (
|
||||||
|
<section aria-labelledby="reviews-title" className="py-4">
|
||||||
|
<section className={glassSurfaceCard("p-4")}>
|
||||||
|
<div className="flex justify-between items-center pb-[25px]">
|
||||||
|
<h2 className="text-sm leading-5">{data.TabPassword.Heading}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={(e) => e.preventDefault()} className="grid gap-6 p-2">
|
||||||
|
{data.TabPassword.Inputs.map((input, i) => {
|
||||||
|
return <PasswordField labelText={input.Label} className="bg-container" key={i} htmlFor={input.HtmlName ?? ""} placeholder={input.Placeholder} autoComplete={input.AutoComplete} />;
|
||||||
|
})}
|
||||||
|
|
||||||
|
{data.TabPassword.Rules.map((rule, i) => {
|
||||||
|
return (
|
||||||
|
<section key={i} className="bg-background rounded-normal p-4 text-xs flex flex-col gap-2 font-light">
|
||||||
|
<h6 className="text-sm mb-2">{rule.Heading}</h6>
|
||||||
|
{rule.Items.map((item, i) => {
|
||||||
|
return <li key={i}>{item}</li>;
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Button className="mt-4">
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
<TickCircle className="stroke-white" size={20} />
|
||||||
|
<span className="mt-0.5">{data.TabPassword.ButtonSubmit}</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const authenticatorTab = () => {
|
||||||
|
return (
|
||||||
|
<section aria-labelledby="reviews-title" className="py-4">
|
||||||
|
<div className={glassSurfaceCard("p-4")}>
|
||||||
|
<div className="pb-4">
|
||||||
|
<h2 className="text-sm leading-5">{data.TabAuthenticator.Heading}</h2>
|
||||||
|
<p className="text-xs mt-3 text-disabled-text font-light">{data.TabAuthenticator.Description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Seperator />
|
||||||
|
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="mt-14 bg-blue-100 p-4 place-self-center rounded-full">
|
||||||
|
<Key size={32} className="stroke-blue-500" />
|
||||||
|
</div>
|
||||||
|
<p className="text-sm2 mt-4 font-light place-self-center">{data.TabAuthenticator.Tip}</p>
|
||||||
|
|
||||||
|
<div className="place-self-center mt-2">
|
||||||
|
<Button className="mt-4 px-14 py-2">
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
<TickCircle className="stroke-white" size={20} />
|
||||||
|
<span className="mt-0.5">{data.TabAuthenticator.ButtonSubmit}</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className='bg-container rounded-container shadow-container p-4'>
|
{data.TabAuthenticator.Guides.map((guide, i) => {
|
||||||
<div className='flex justify-between items-center pb-4'>
|
return (
|
||||||
<h2 className='text-sm leading-5'>{data.StatisticalParticipation.Heading}</h2>
|
<section key={i} className="bg-background rounded-normal mt-14 p-4 text-xs flex flex-col gap-2 font-light">
|
||||||
<Switch
|
<h6 className="text-sm">{guide.Heading}</h6>
|
||||||
checked={statisticalParticipation}
|
<p className="text-disabled-text">{guide.Description}</p>
|
||||||
onCheckedChange={handleStatisticalToggle}
|
{guide.Items.map((item, i) => {
|
||||||
id={data.StatisticalParticipation.HtmlName}
|
return (
|
||||||
name={data.StatisticalParticipation.HtmlName}
|
<div key={i}>
|
||||||
className="w-12 h-6 scale-[0.9]"
|
<div className="ml-2 mt-2 bg-blue-100 inline-block size-6 text-center pt-[5px] pr-px text-blue-500 rounded-full">{item.Mark}</div>
|
||||||
/>
|
<span>{item.Label}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-xs text-disabled-text font-light'>
|
);
|
||||||
{data.StatisticalParticipation.Description}
|
})}
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
{guide.Notes.map((note, i) => {
|
||||||
|
return (
|
||||||
|
<div key={i} className="mt-4 p-3 text-yellow-800 rounded-lg bg-yellow-50 border border-solid border-yellow-200">
|
||||||
|
{note}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-y-auto h-full noscrollbar flex flex-col gap-6">
|
||||||
|
<div className="grid grid-cols-3 items-center">
|
||||||
|
<span></span>
|
||||||
|
<h1 className="text-sm2 place-self-center font-medium">{data.Heading}</h1>
|
||||||
|
<ArrowLeft
|
||||||
|
className="cursor-pointer place-self-end"
|
||||||
|
size="24"
|
||||||
|
color="currentColor"
|
||||||
|
onClick={() => {
|
||||||
|
router.back();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className={glassSurfaceCard("p-4")}>
|
||||||
|
<div className="flex justify-between items-center pb-4">
|
||||||
|
<h2 className="text-sm leading-5">{data.StatisticalParticipation.Heading}</h2>
|
||||||
|
<Switch
|
||||||
|
checked={statisticalParticipation}
|
||||||
|
onCheckedChange={handleStatisticalToggle}
|
||||||
|
id={data.StatisticalParticipation.HtmlName}
|
||||||
|
name={data.StatisticalParticipation.HtmlName}
|
||||||
|
className="w-12 h-6 scale-[0.9]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-disabled-text font-light">{data.StatisticalParticipation.Description}</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserSettingsIndex
|
export default UserSettingsIndex;
|
||||||
|
|||||||
@@ -1,111 +1,85 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Button from '@/components/button/PrimaryButton';
|
import Button from "@/components/button/PrimaryButton";
|
||||||
import InputField from '@/components/input/InputField';
|
import InputField from "@/components/input/InputField";
|
||||||
import { ef, ue } from '@/lib/helpers/utfNumbers';
|
import { ef, ue } from "@/lib/helpers/utfNumbers";
|
||||||
import { ArrowLeft, ArrowRight, WalletMinus } from 'iconsax-react';
|
import { glassSurfaceCard } from "@/lib/styles/glassSurface";
|
||||||
import { useRouter } from 'next/navigation';
|
import { ArrowLeft, ArrowRight, WalletMinus } from "iconsax-react";
|
||||||
import React, { useState } from 'react'
|
import { useRouter } from "next/navigation";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
type Props = object
|
type Props = object;
|
||||||
|
|
||||||
function UserWalletIndex({ }: Props) {
|
function UserWalletIndex({}: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [chargeValue, setChargeValue] = useState<string>('0');
|
const [chargeValue, setChargeValue] = useState<string>("0");
|
||||||
const chargeAction = (e: React.FormEvent<HTMLFormElement>) => {
|
const chargeAction = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = new FormData(e.currentTarget)
|
const data = new FormData(e.currentTarget);
|
||||||
const value = Number(ue(String(data.get('chargeValue') ?? 0))) + 0;
|
const value = Number(ue(String(data.get("chargeValue") ?? 0))) + 0;
|
||||||
console.log("Charge for: ", value);
|
console.log("Charge for: ", value);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='overflow-y-auto h-full noscrollbar flex flex-col'>
|
<div className="overflow-y-auto h-full noscrollbar flex flex-col">
|
||||||
<div className='grid grid-cols-3 items-center'>
|
<div className="grid grid-cols-3 items-center">
|
||||||
<span></span>
|
<span></span>
|
||||||
<h1 className='text-sm2 place-self-center font-medium'>پروفایل کاربری</h1>
|
<h1 className="text-sm2 place-self-center font-medium">پروفایل کاربری</h1>
|
||||||
<ArrowLeft
|
<ArrowLeft
|
||||||
className='cursor-pointer place-self-end'
|
className="cursor-pointer place-self-end"
|
||||||
size='24'
|
size="24"
|
||||||
color='currentColor'
|
color="currentColor"
|
||||||
onClick={() => { router.back() }}
|
onClick={() => {
|
||||||
/>
|
router.back();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={chargeAction} className={glassSurfaceCard("mt-8 flex-1 h-full w-full py-6 px-4 flex flex-col justify-between")}>
|
||||||
|
<div className="">
|
||||||
|
<div className="flex items-center justify-center text-center gap-4 pb-6 border-b-[1.5px] border-border">
|
||||||
|
<div className="block">
|
||||||
|
<div className="font-medium text-sm flex items-center gap-2">
|
||||||
|
<WalletMinus className="size-5 stroke-foreground mb-0.5" />
|
||||||
|
موجودی کیف پول
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-primary mt-3 inline-flex dark:text-neutral-200 items-center gap-2">{ef("250.000 تومان")}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form
|
<div className="grid gap-3 mt-6">
|
||||||
onSubmit={chargeAction}
|
<div className="inline-flex items-center mb-3 gap-2.5 text-sm2">برای شارژ کیف پول خود میتوانید بر روی یکی از مبالغ زیر کلیک کنید و یا مبلغ دلخواه خود را وارد کنید</div>
|
||||||
className="mt-8 flex-1 h-full bg-container rounded-container w-full box-shadow-normal py-6 px-4 flex flex-col justify-between">
|
<Button type="button" onClick={() => setChargeValue("50000")} className="bg-disabled! text-sm2 text-foreground! flex justify-between items-center">
|
||||||
|
<ArrowRight className="stroke-foreground size-5" />
|
||||||
<div className="">
|
<span className="mt-0.5">{ef("50,000 تومان")}</span>
|
||||||
<div className="flex items-center justify-center text-center gap-4 pb-6 border-b-[1.5px] border-border">
|
</Button>
|
||||||
<div className='block'>
|
<Button type="button" onClick={() => setChargeValue("150000")} className="bg-disabled! text-sm2 text-foreground! flex justify-between items-center">
|
||||||
<div className='font-medium text-sm flex items-center gap-2'>
|
<ArrowRight className="stroke-foreground size-5" />
|
||||||
<WalletMinus className='size-5 stroke-foreground mb-0.5' />
|
<span className="mt-0.5">{ef("150,000 تومان")}</span>
|
||||||
موجودی کیف پول
|
</Button>
|
||||||
</div>
|
<Button type="button" onClick={() => setChargeValue("300000")} className="bg-disabled! text-sm2 text-foreground! flex justify-between items-center">
|
||||||
<div className='text-sm text-primary mt-3 inline-flex dark:text-neutral-200 items-center gap-2'>
|
<ArrowRight className="stroke-foreground size-5" />
|
||||||
{ef('250.000 تومان')}
|
<span className="mt-0.5">{ef("300,000 تومان")}</span>
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
<InputField
|
||||||
</div>
|
className="bg-container mt-7"
|
||||||
|
labelText="مبلغ دلخواه (تومان)"
|
||||||
<div className="grid gap-3 mt-6">
|
dir="ltr"
|
||||||
<div className='inline-flex items-center mb-3 gap-2.5 text-sm2'>
|
inputMode="decimal"
|
||||||
برای شارژ کیف پول خود میتوانید بر روی یکی از مبالغ زیر کلیک کنید و یا مبلغ دلخواه خود را وارد کنید
|
htmlFor="chargeValue"
|
||||||
</div>
|
placeholder=""
|
||||||
<Button
|
value={ef(chargeValue)}
|
||||||
type='button'
|
onChange={(e) => setChargeValue(e.target.value)}
|
||||||
onClick={() => setChargeValue('50000')}
|
/>
|
||||||
className='!bg-disabled text-sm2 !text-foreground flex justify-between items-center'
|
</div>
|
||||||
>
|
|
||||||
<ArrowRight className='stroke-foreground size-5' />
|
|
||||||
<span className='mt-0.5'>
|
|
||||||
{ef('50,000 تومان')}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
onClick={() => setChargeValue('150000')}
|
|
||||||
className='!bg-disabled text-sm2 !text-foreground flex justify-between items-center'
|
|
||||||
>
|
|
||||||
<ArrowRight className='stroke-foreground size-5' />
|
|
||||||
<span className='mt-0.5'>
|
|
||||||
{ef('150,000 تومان')}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
onClick={() => setChargeValue('300000')}
|
|
||||||
className='!bg-disabled text-sm2 !text-foreground flex justify-between items-center'
|
|
||||||
>
|
|
||||||
<ArrowRight className='stroke-foreground size-5' />
|
|
||||||
<span className='mt-0.5'>
|
|
||||||
{ef('300,000 تومان')}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
<InputField
|
|
||||||
className='bg-container mt-7'
|
|
||||||
labelText='مبلغ دلخواه (تومان)'
|
|
||||||
dir='ltr'
|
|
||||||
inputMode='decimal'
|
|
||||||
htmlFor='chargeValue'
|
|
||||||
placeholder=''
|
|
||||||
value={ef(chargeValue)}
|
|
||||||
onChange={(e) => setChargeValue(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='w-full text-center mt-6'>
|
|
||||||
<Button
|
|
||||||
type='submit'
|
|
||||||
>
|
|
||||||
شارژ کیف پول
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
|
<div className="w-full text-center mt-6">
|
||||||
|
<Button type="submit">شارژ کیف پول</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserWalletIndex
|
export default UserWalletIndex;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import ActiveChecker from "@/components/ActiveChecker";
|
import ActiveChecker from "@/components/ActiveChecker";
|
||||||
|
import AppBackground from "@/components/background/AppBackground";
|
||||||
import CartChecker from "@/components/CartChecker";
|
import CartChecker from "@/components/CartChecker";
|
||||||
import RestaurantHeadManager from "@/components/RestaurantHeadManager";
|
import RestaurantHeadManager from "@/components/RestaurantHeadManager";
|
||||||
import RestaurantNotFoundGuard from "@/components/RestaurantNotFoundGuard";
|
import RestaurantNotFoundGuard from "@/components/RestaurantNotFoundGuard";
|
||||||
import PreferenceWrapper from "@/components/wrapper/PreferenceWrapper";
|
import PreferenceWrapper from "@/components/wrapper/PreferenceWrapper";
|
||||||
|
import { PatternBackgroundProvider } from "@/components/background/PatternBackgroundProvider";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -15,7 +17,12 @@ export default function RestaurantLayoutClient({ children }: Props) {
|
|||||||
<>
|
<>
|
||||||
<RestaurantHeadManager />
|
<RestaurantHeadManager />
|
||||||
<RestaurantNotFoundGuard>
|
<RestaurantNotFoundGuard>
|
||||||
<PreferenceWrapper>{children}</PreferenceWrapper>
|
<PatternBackgroundProvider>
|
||||||
|
<PreferenceWrapper>
|
||||||
|
<AppBackground />
|
||||||
|
{children}
|
||||||
|
</PreferenceWrapper>
|
||||||
|
</PatternBackgroundProvider>
|
||||||
</RestaurantNotFoundGuard>
|
</RestaurantNotFoundGuard>
|
||||||
<CartChecker />
|
<CartChecker />
|
||||||
<ActiveChecker />
|
<ActiveChecker />
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: "Invalid URL" }, { status: 400 });
|
return NextResponse.json({ error: "Invalid URL" }, { status: 400 });
|
||||||
}
|
}
|
||||||
const res = await fetchWithTimeout(url, {
|
const res = await fetchWithTimeout(url, {
|
||||||
timeoutMs: 5000,
|
timeoutMs: 15_000,
|
||||||
headers: { Accept: "image/svg+xml, text/xml, text/plain" },
|
headers: { Accept: "image/svg+xml, text/xml, text/plain" },
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "../../public/assets/css/fonts.css";
|
@import "../../public/assets/css/fonts.css";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
@import "../styles/glass.css";
|
||||||
|
|
||||||
/* @custom-variant dark (&:is(.dark *)); */
|
/* @custom-variant dark (&:is(.dark *)); */
|
||||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||||
@@ -396,3 +397,37 @@ html[data-theme="dark"] {
|
|||||||
-webkit-mask: url("/assets/images/fast-delivery.svg") no-repeat center / contain;
|
-webkit-mask: url("/assets/images/fast-delivery.svg") no-repeat center / contain;
|
||||||
mask: url("/assets/images/fast-delivery.svg") no-repeat center / contain;
|
mask: url("/assets/images/fast-delivery.svg") no-repeat center / contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-pattern-bg="true"] #root {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-pattern-bg="true"] [data-slot="dialog-content"] {
|
||||||
|
background-color: var(--container) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-pattern-bg="true"] [data-slot="splash-screen"] {
|
||||||
|
background-color: var(--container) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-pattern-bg="true"] .pattern-secondary-bg {
|
||||||
|
background-color: var(--primary-light) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-image-bg="true"] #root {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-image-bg="true"] [data-slot="dialog-content"] {
|
||||||
|
background-color: var(--container) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-image-bg="true"] [data-slot="splash-screen"] {
|
||||||
|
background-color: var(--container) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-image-bg="true"] .pattern-secondary-bg {
|
||||||
|
background-color: var(--primary-light) !important;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import initTranslations from '@/lib/i18n';
|
|||||||
import TranslationsProvider from "@/components/utils/TranslationsProdiver";
|
import TranslationsProvider from "@/components/utils/TranslationsProdiver";
|
||||||
import ToastContainer from "@/components/Toast";
|
import ToastContainer from "@/components/Toast";
|
||||||
import { ThemeColorSetter } from "@/components/ThemeColorSetter";
|
import { ThemeColorSetter } from "@/components/ThemeColorSetter";
|
||||||
|
import { ThemeBootScript } from "@/components/ThemeBootScript";
|
||||||
import PwaServiceWorker from "@/components/PwaServiceWorker";
|
import PwaServiceWorker from "@/components/PwaServiceWorker";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -48,6 +49,7 @@ export default async function RootLayout({
|
|||||||
|
|
||||||
className={`antialiased bg-background h-svh overflow-hidden`}
|
className={`antialiased bg-background h-svh overflow-hidden`}
|
||||||
>
|
>
|
||||||
|
<ThemeBootScript />
|
||||||
<ReactQueryProvider>
|
<ReactQueryProvider>
|
||||||
<TranslationsProvider
|
<TranslationsProvider
|
||||||
namespaces={i18nNamespaces}
|
namespaces={i18nNamespaces}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import LoadingOverlay from '@/components/overlays/LoadingOverlay'
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
function Loading() {
|
|
||||||
return (
|
|
||||||
<LoadingOverlay />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Loading
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { getThemeBootScript } from "@/lib/helpers/themeBootScript";
|
||||||
|
|
||||||
|
export function ThemeBootScript() {
|
||||||
|
return (
|
||||||
|
<script
|
||||||
|
id="theme-boot"
|
||||||
|
dangerouslySetInnerHTML={{ __html: getThemeBootScript() }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect } from 'react'
|
import { useLayoutEffect } from 'react'
|
||||||
import { hexToOklch, calculateBrightness } from '@/lib/helpers/colorUtils'
|
import { hexToOklch, calculateBrightness } from '@/lib/helpers/colorUtils'
|
||||||
|
import { getCachedMenuColor, getRestaurantSlugFromPath, getThemeCache, applyCachedThemeToDom } from '@/lib/helpers/themeCache'
|
||||||
|
|
||||||
export function ThemeColorSetter() {
|
export function ThemeColorSetter() {
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
try {
|
try {
|
||||||
const savedColor = localStorage.getItem('theme-primary-color')
|
const slug = getRestaurantSlugFromPath()
|
||||||
|
const cachedTheme = slug ? getThemeCache(slug) : null
|
||||||
|
if (cachedTheme) {
|
||||||
|
applyCachedThemeToDom(cachedTheme)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedColor = getCachedMenuColor(slug)
|
||||||
if (savedColor) {
|
if (savedColor) {
|
||||||
const oklchColor = hexToOklch(savedColor)
|
const oklchColor = hexToOklch(savedColor)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { usePatternBackground } from "@/components/background/PatternBackgroundProvider";
|
||||||
|
import usePreference from "@/hooks/helpers/usePreference";
|
||||||
|
import { THEME_BG_ELEMENT_ID } from "@/lib/helpers/themeCache";
|
||||||
|
import { useLayoutEffect } from "react";
|
||||||
|
|
||||||
|
const THEME_BG_IMG_ID = "cached-theme-bg-img";
|
||||||
|
const THEME_BG_OVERLAY_ID = "cached-theme-bg-overlay";
|
||||||
|
|
||||||
|
export default function AppBackground() {
|
||||||
|
const { state: nightMode } = usePreference("night-mode", false);
|
||||||
|
const { isPattern, backgroundStyle, isCustomImage, customImageSettings } =
|
||||||
|
usePatternBackground();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (nightMode || (!isPattern && !isCustomImage)) {
|
||||||
|
document.getElementById(THEME_BG_ELEMENT_ID)?.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let element = document.getElementById(THEME_BG_ELEMENT_ID);
|
||||||
|
if (!element) {
|
||||||
|
element = document.createElement("div");
|
||||||
|
element.id = THEME_BG_ELEMENT_ID;
|
||||||
|
element.setAttribute("aria-hidden", "true");
|
||||||
|
element.style.position = "fixed";
|
||||||
|
element.style.inset = "0";
|
||||||
|
element.style.pointerEvents = "none";
|
||||||
|
element.style.zIndex = "0";
|
||||||
|
document.body.appendChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPattern) {
|
||||||
|
element.style.overflow = "";
|
||||||
|
document.getElementById(THEME_BG_IMG_ID)?.remove();
|
||||||
|
document.getElementById(THEME_BG_OVERLAY_ID)?.remove();
|
||||||
|
Object.assign(element.style, backgroundStyle as Record<string, string>);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCustomImage && customImageSettings) {
|
||||||
|
const { bgUrl, bgBlur, bgOpacity, bgOverlay } = customImageSettings;
|
||||||
|
|
||||||
|
element.style.backgroundColor = "";
|
||||||
|
element.style.backgroundImage = "";
|
||||||
|
element.style.overflow = "hidden";
|
||||||
|
|
||||||
|
let imgLayer = document.getElementById(THEME_BG_IMG_ID);
|
||||||
|
if (!imgLayer) {
|
||||||
|
imgLayer = document.createElement("div");
|
||||||
|
imgLayer.id = THEME_BG_IMG_ID;
|
||||||
|
imgLayer.style.position = "absolute";
|
||||||
|
imgLayer.style.inset = "-40px";
|
||||||
|
imgLayer.style.backgroundSize = "cover";
|
||||||
|
imgLayer.style.backgroundPosition = "center";
|
||||||
|
imgLayer.style.backgroundRepeat = "no-repeat";
|
||||||
|
element.appendChild(imgLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
imgLayer.style.backgroundImage = `url("${bgUrl}")`;
|
||||||
|
imgLayer.style.filter = bgBlur ? `blur(${bgBlur}px)` : "";
|
||||||
|
imgLayer.style.opacity =
|
||||||
|
bgOpacity != null ? String(bgOpacity / 100) : "1";
|
||||||
|
|
||||||
|
let overlayLayer = document.getElementById(THEME_BG_OVERLAY_ID);
|
||||||
|
if (!overlayLayer) {
|
||||||
|
overlayLayer = document.createElement("div");
|
||||||
|
overlayLayer.id = THEME_BG_OVERLAY_ID;
|
||||||
|
overlayLayer.style.position = "absolute";
|
||||||
|
overlayLayer.style.inset = "0";
|
||||||
|
element.appendChild(overlayLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlayOpacity =
|
||||||
|
bgOverlay != null ? Number(bgOverlay) / 100 : 0;
|
||||||
|
overlayLayer.style.backgroundColor =
|
||||||
|
overlayOpacity > 0 ? `rgba(0,0,0,${overlayOpacity})` : "";
|
||||||
|
}
|
||||||
|
}, [backgroundStyle, isCustomImage, isPattern, customImageSettings, nightMode]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useGetAbout } from "@/app/[name]/(Main)/about/hooks/useAboutData";
|
||||||
|
import {
|
||||||
|
applyColorBackgroundCssVariables,
|
||||||
|
applyCustomImageCssVariables,
|
||||||
|
applyPatternCssVariables,
|
||||||
|
clearCustomImageCssVariables,
|
||||||
|
clearPatternCssVariables,
|
||||||
|
colorizeSvg,
|
||||||
|
hexToRgba,
|
||||||
|
normalizeBgNumber,
|
||||||
|
PATTERN_BACKGROUND_OPACITY,
|
||||||
|
resolveBgType,
|
||||||
|
} from "@/lib/helpers/backgroundUtils";
|
||||||
|
import usePreference from "@/hooks/helpers/usePreference";
|
||||||
|
import {
|
||||||
|
applyCachedThemeToDom,
|
||||||
|
buildThemeCacheFromRestaurant,
|
||||||
|
clearRestaurantBackgroundOverrides,
|
||||||
|
getThemeCache,
|
||||||
|
removeCachedThemeBackground,
|
||||||
|
setThemeCache,
|
||||||
|
svgToDataUrl,
|
||||||
|
themeBackgroundMatches,
|
||||||
|
type CachedRestaurantTheme,
|
||||||
|
} from "@/lib/helpers/themeCache";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
type CSSProperties,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
const DEFAULT_MENU_COLOR = "#1E3A8A";
|
||||||
|
|
||||||
|
export type CustomImageSettings = {
|
||||||
|
bgUrl: string;
|
||||||
|
bgBlur: number | null;
|
||||||
|
bgOpacity: number | null;
|
||||||
|
bgOverlay: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PatternBackgroundContextValue = {
|
||||||
|
isPattern: boolean;
|
||||||
|
isCustomImage: boolean;
|
||||||
|
backgroundStyle: CSSProperties;
|
||||||
|
customImageSettings: CustomImageSettings | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PatternBackgroundContext = createContext<PatternBackgroundContextValue>({
|
||||||
|
isPattern: false,
|
||||||
|
isCustomImage: false,
|
||||||
|
backgroundStyle: {},
|
||||||
|
customImageSettings: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function PatternBackgroundProvider({ children }: { children: ReactNode }) {
|
||||||
|
const { name: restaurantSlug } = useParams<{ name: string }>();
|
||||||
|
const { state: nightMode } = usePreference("night-mode", false);
|
||||||
|
const { data: aboutData } = useGetAbout();
|
||||||
|
const restaurant = aboutData?.data;
|
||||||
|
const [hydratedCache, setHydratedCache] = useState<CachedRestaurantTheme | null>(null);
|
||||||
|
const [patternImageUrl, setPatternImageUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const bgType = restaurant
|
||||||
|
? resolveBgType(restaurant)
|
||||||
|
: hydratedCache
|
||||||
|
? resolveBgType(hydratedCache)
|
||||||
|
: "color";
|
||||||
|
|
||||||
|
const isPattern = !nightMode && bgType === "pattern";
|
||||||
|
const isCustomImage = !nightMode && bgType === "custom";
|
||||||
|
const menuColor =
|
||||||
|
restaurant?.menuColor || hydratedCache?.menuColor || DEFAULT_MENU_COLOR;
|
||||||
|
const baseTint = hexToRgba(menuColor, PATTERN_BACKGROUND_OPACITY);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!restaurantSlug) return;
|
||||||
|
|
||||||
|
const cached = getThemeCache(restaurantSlug);
|
||||||
|
setHydratedCache(cached);
|
||||||
|
|
||||||
|
if (!cached) return;
|
||||||
|
|
||||||
|
applyCachedThemeToDom(cached);
|
||||||
|
if (!nightMode && cached.patternDataUrl) {
|
||||||
|
setPatternImageUrl(cached.patternDataUrl);
|
||||||
|
}
|
||||||
|
}, [nightMode, restaurantSlug]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!restaurantSlug) {
|
||||||
|
clearPatternCssVariables();
|
||||||
|
clearCustomImageCssVariables();
|
||||||
|
removeCachedThemeBackground();
|
||||||
|
setPatternImageUrl(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nightMode) {
|
||||||
|
clearPatternCssVariables();
|
||||||
|
clearCustomImageCssVariables();
|
||||||
|
clearRestaurantBackgroundOverrides();
|
||||||
|
setPatternImageUrl(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!restaurant) return;
|
||||||
|
|
||||||
|
const themeBase = buildThemeCacheFromRestaurant(restaurant);
|
||||||
|
const currentBgType = themeBase.bgType;
|
||||||
|
|
||||||
|
if (currentBgType === "color") {
|
||||||
|
clearCustomImageCssVariables();
|
||||||
|
removeCachedThemeBackground();
|
||||||
|
setPatternImageUrl(null);
|
||||||
|
applyColorBackgroundCssVariables(menuColor);
|
||||||
|
setThemeCache(restaurantSlug, { ...themeBase, patternDataUrl: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentBgType === "custom") {
|
||||||
|
clearPatternCssVariables();
|
||||||
|
setPatternImageUrl(null);
|
||||||
|
applyCustomImageCssVariables();
|
||||||
|
setThemeCache(restaurantSlug, { ...themeBase, patternDataUrl: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCustomImageCssVariables();
|
||||||
|
|
||||||
|
applyPatternCssVariables(menuColor);
|
||||||
|
|
||||||
|
if (!themeBase.bgUrl) {
|
||||||
|
setPatternImageUrl(null);
|
||||||
|
setThemeCache(restaurantSlug, {
|
||||||
|
...themeBase,
|
||||||
|
patternDataUrl: null,
|
||||||
|
});
|
||||||
|
const saved = getThemeCache(restaurantSlug);
|
||||||
|
if (saved) {
|
||||||
|
applyCachedThemeToDom(saved);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = getThemeCache(restaurantSlug);
|
||||||
|
const cacheHit =
|
||||||
|
cached &&
|
||||||
|
themeBackgroundMatches(cached, restaurant) &&
|
||||||
|
cached.isPattern &&
|
||||||
|
cached.patternDataUrl;
|
||||||
|
|
||||||
|
if (cacheHit) {
|
||||||
|
setPatternImageUrl(cached.patternDataUrl!);
|
||||||
|
setThemeCache(restaurantSlug, {
|
||||||
|
...themeBase,
|
||||||
|
patternDataUrl: cached.patternDataUrl,
|
||||||
|
});
|
||||||
|
applyCachedThemeToDom(cached);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
const proxyUrl = `/${restaurantSlug}/api/proxy-svg?url=${encodeURIComponent(themeBase.bgUrl)}`;
|
||||||
|
|
||||||
|
fetch(proxyUrl)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch background pattern");
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then((svg) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
const dataUrl = svgToDataUrl(colorizeSvg(svg, menuColor));
|
||||||
|
setPatternImageUrl(dataUrl);
|
||||||
|
|
||||||
|
const nextCache = {
|
||||||
|
...themeBase,
|
||||||
|
patternDataUrl: dataUrl,
|
||||||
|
};
|
||||||
|
setThemeCache(restaurantSlug, nextCache);
|
||||||
|
|
||||||
|
const saved = getThemeCache(restaurantSlug);
|
||||||
|
if (saved) {
|
||||||
|
applyCachedThemeToDom(saved);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled && cached?.patternDataUrl) {
|
||||||
|
setPatternImageUrl(cached.patternDataUrl);
|
||||||
|
applyCachedThemeToDom(cached);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [menuColor, nightMode, restaurant, restaurantSlug]);
|
||||||
|
|
||||||
|
const backgroundStyle = useMemo<CSSProperties>(
|
||||||
|
() => ({
|
||||||
|
backgroundColor: baseTint,
|
||||||
|
...(patternImageUrl
|
||||||
|
? {
|
||||||
|
backgroundImage: `url("${patternImageUrl}")`,
|
||||||
|
backgroundRepeat: "repeat",
|
||||||
|
backgroundSize: "auto",
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
}),
|
||||||
|
[baseTint, patternImageUrl],
|
||||||
|
);
|
||||||
|
|
||||||
|
const customImageSettings = useMemo<CustomImageSettings | null>(() => {
|
||||||
|
const src = restaurant?.bgUrl || hydratedCache?.bgUrl;
|
||||||
|
if (!isCustomImage || !src) return null;
|
||||||
|
return {
|
||||||
|
bgUrl: src,
|
||||||
|
bgBlur: normalizeBgNumber(restaurant?.bgBlur ?? hydratedCache?.bgBlur),
|
||||||
|
bgOpacity: normalizeBgNumber(restaurant?.bgOpacity ?? hydratedCache?.bgOpacity),
|
||||||
|
bgOverlay: restaurant?.bgOverlay ?? hydratedCache?.bgOverlay ?? null,
|
||||||
|
};
|
||||||
|
}, [isCustomImage, restaurant, hydratedCache]);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({ isPattern, isCustomImage, backgroundStyle, customImageSettings }),
|
||||||
|
[backgroundStyle, isPattern, isCustomImage, customImageSettings],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PatternBackgroundContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</PatternBackgroundContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePatternBackground() {
|
||||||
|
return useContext(PatternBackgroundContext);
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import clsx from "clsx";
|
|||||||
import { motion, Variants } from "framer-motion";
|
import { motion, Variants } from "framer-motion";
|
||||||
import { Icon, SearchNormal } from "iconsax-react";
|
import { Icon, SearchNormal } from "iconsax-react";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
export interface ComboboxOption {
|
export interface ComboboxOption {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -28,6 +29,12 @@ type Props = {
|
|||||||
) => void;
|
) => void;
|
||||||
} & React.HTMLAttributes<HTMLDivElement>;
|
} & React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
|
type DropdownPosition = {
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
width: number;
|
||||||
|
};
|
||||||
|
|
||||||
function Combobox({
|
function Combobox({
|
||||||
title,
|
title,
|
||||||
options,
|
options,
|
||||||
@@ -37,17 +44,53 @@ function Combobox({
|
|||||||
selectedId,
|
selectedId,
|
||||||
searchable = true,
|
searchable = true,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
|
className,
|
||||||
...props
|
...props
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [expand, setExpand] = useState(expanded);
|
const [expand, setExpand] = useState(expanded);
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [searchValue, setSearchValue] = useState("");
|
||||||
|
const [dropdownPosition, setDropdownPosition] = useState<DropdownPosition | null>(null);
|
||||||
const boxRef = useRef<HTMLDivElement>(null);
|
const boxRef = useRef<HTMLDivElement>(null);
|
||||||
|
const triggerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const updateDropdownPosition = useCallback(() => {
|
||||||
|
if (!triggerRef.current) return;
|
||||||
|
|
||||||
|
const rect = triggerRef.current.getBoundingClientRect();
|
||||||
|
setDropdownPosition({
|
||||||
|
top: rect.bottom + 4,
|
||||||
|
left: rect.left,
|
||||||
|
width: rect.width,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!expand) {
|
||||||
|
setDropdownPosition(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDropdownPosition();
|
||||||
|
window.addEventListener("scroll", updateDropdownPosition, true);
|
||||||
|
window.addEventListener("resize", updateDropdownPosition);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", updateDropdownPosition, true);
|
||||||
|
window.removeEventListener("resize", updateDropdownPosition);
|
||||||
|
};
|
||||||
|
}, [expand, updateDropdownPosition]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (boxRef.current && !boxRef.current.contains(event.target as Node)) {
|
const target = event.target as Node;
|
||||||
setExpand(false);
|
if (
|
||||||
|
boxRef.current?.contains(target) ||
|
||||||
|
dropdownRef.current?.contains(target)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
setExpand(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (expand) {
|
if (expand) {
|
||||||
@@ -72,12 +115,14 @@ function Combobox({
|
|||||||
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||||
index: number,
|
index: number,
|
||||||
) => {
|
) => {
|
||||||
e.stopPropagation(); // prevent toggle
|
e.stopPropagation();
|
||||||
onSelectionChange(e, index);
|
onSelectionChange(e, index);
|
||||||
setExpand(false);
|
setExpand(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedOption = options.find((x) => x.id === selectedId);
|
const selectedOption = options.find((x) => x.id === selectedId);
|
||||||
|
const filteredOptions = options.filter((v) => v.title?.includes(searchValue));
|
||||||
|
|
||||||
const activeIcon =
|
const activeIcon =
|
||||||
selectedOption?.icon &&
|
selectedOption?.icon &&
|
||||||
React.createElement(selectedOption.icon, {
|
React.createElement(selectedOption.icon, {
|
||||||
@@ -93,7 +138,7 @@ function Combobox({
|
|||||||
collapse: {
|
collapse: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
scale: 0.95,
|
scale: 0.95,
|
||||||
pointerEvents: "none", // disable clicks
|
pointerEvents: "none",
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.1,
|
duration: 0.1,
|
||||||
ease: "easeInOut",
|
ease: "easeInOut",
|
||||||
@@ -102,7 +147,7 @@ function Combobox({
|
|||||||
expand: {
|
expand: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
pointerEvents: "auto", // re-enable clicks
|
pointerEvents: "auto",
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.1,
|
duration: 0.1,
|
||||||
ease: "easeInOut",
|
ease: "easeInOut",
|
||||||
@@ -110,13 +155,84 @@ function Combobox({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dropdown =
|
||||||
|
expand &&
|
||||||
|
dropdownPosition &&
|
||||||
|
typeof document !== "undefined"
|
||||||
|
? createPortal(
|
||||||
|
<motion.div
|
||||||
|
ref={dropdownRef}
|
||||||
|
data-expand={expand}
|
||||||
|
aria-hidden={!expand}
|
||||||
|
className="fixed text-foreground bg-container rounded-xl border border-border shadow-lg z-9999"
|
||||||
|
style={{
|
||||||
|
top: dropdownPosition.top,
|
||||||
|
left: dropdownPosition.left,
|
||||||
|
width: dropdownPosition.width,
|
||||||
|
}}
|
||||||
|
role="listbox"
|
||||||
|
aria-label={`${title} options`}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
initial="collapse"
|
||||||
|
animate="expand"
|
||||||
|
variants={variants}
|
||||||
|
>
|
||||||
|
{searchable && (
|
||||||
|
<div className="w-full flex gap-2 border-b border-border px-3 items-center">
|
||||||
|
<SearchNormal size={16} className="stroke-gray-400" />
|
||||||
|
<input
|
||||||
|
placeholder="جستجو ..."
|
||||||
|
className="w-full outline-none text-sm2 pb-2 pt-3"
|
||||||
|
onChange={searchChanged}
|
||||||
|
value={searchValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col p-1">
|
||||||
|
{filteredOptions.map((v) => {
|
||||||
|
const originalIndex = options.findIndex((o) => o.id === v.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={(e) => setSelection(e, originalIndex)}
|
||||||
|
key={v.id}
|
||||||
|
data-selected={v.id === selectedId}
|
||||||
|
className="flex gap-3 items-center justify-end px-2 py-2 cursor-pointer hover:bg-current/10 rounded-md last:rounded-b-xl"
|
||||||
|
tabIndex={0}
|
||||||
|
role="option"
|
||||||
|
aria-selected={v.id === selectedId}
|
||||||
|
>
|
||||||
|
{v?.imagePath ? (
|
||||||
|
<span className="icon-delivery shrink-0" aria-hidden />
|
||||||
|
) : (
|
||||||
|
v?.icon &&
|
||||||
|
React.createElement(v.icon, {
|
||||||
|
size: 16,
|
||||||
|
color: "currentColor",
|
||||||
|
className:
|
||||||
|
"inline-block shrink-0 text-primary dark:text-foreground",
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
<span className="text-sm2 tracking-[0.13px] leading-5 flex-1 text-right">
|
||||||
|
{v.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={boxRef}
|
ref={boxRef}
|
||||||
className={clsx("relative", props.className ?? "")}
|
className={clsx("relative", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
ref={triggerRef}
|
||||||
className="flex w-full h-11 items-center justify-end gap-2 px-3 py-4 relative bg-container/29 rounded-xl border border-solid border-border cursor-pointer"
|
className="flex w-full h-11 items-center justify-end gap-2 px-3 py-4 relative bg-container/29 rounded-xl border border-solid border-border cursor-pointer"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={toggleExpand}
|
onClick={toggleExpand}
|
||||||
@@ -163,59 +279,7 @@ function Combobox({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
{dropdown}
|
||||||
data-expand={expand}
|
|
||||||
aria-hidden={!expand}
|
|
||||||
className="absolute top-full left-0 w-full mt-1 text-foreground bg-container rounded-xl outline outline-solid outline-border shadow-lg z-10"
|
|
||||||
role="listbox"
|
|
||||||
aria-label={`${title} options`}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
initial="collapse"
|
|
||||||
animate={expand ? "expand" : "collapse"}
|
|
||||||
variants={variants}
|
|
||||||
>
|
|
||||||
{searchable && (
|
|
||||||
<div className="w-full flex gap-2 border-b border-border px-3 items-center">
|
|
||||||
<SearchNormal size={16} className="stroke-gray-400" />
|
|
||||||
<input
|
|
||||||
placeholder="جستجو ..."
|
|
||||||
className="w-full outline-none text-sm2 pb-2 pt-3"
|
|
||||||
onChange={searchChanged}
|
|
||||||
value={searchValue}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="p-1">
|
|
||||||
{options
|
|
||||||
.filter((v) => v.title?.includes(searchValue))
|
|
||||||
.map((v, i) => (
|
|
||||||
<div
|
|
||||||
onClick={(e) => setSelection(e, i)}
|
|
||||||
key={v.id}
|
|
||||||
data-selected={v.id === selectedId}
|
|
||||||
className="flex gap-3 items-center justify-end px-2 py-1 cursor-pointer hover:bg-current/10 rounded-md last:rounded-b-xl"
|
|
||||||
tabIndex={0}
|
|
||||||
role="option"
|
|
||||||
aria-selected
|
|
||||||
>
|
|
||||||
{v?.imagePath ? (
|
|
||||||
<span className="icon-delivery mr-1" aria-hidden />
|
|
||||||
) : (
|
|
||||||
v?.icon &&
|
|
||||||
React.createElement(v.icon, {
|
|
||||||
size: 16,
|
|
||||||
color: "currentColor",
|
|
||||||
className:
|
|
||||||
"inline-block mr-1 text-primary dark:text-foreground",
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
<span className="text-sm2 tracking-[0.13px] leading-5 w-full mt-1">
|
|
||||||
{v.title}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,38 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react'
|
import clsx from "clsx";
|
||||||
import Tooltip from '../utils/Tooltip';
|
import React from "react";
|
||||||
import clsx from 'clsx';
|
import Tooltip from "../utils/Tooltip";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
htmlFor: string;
|
htmlFor: string;
|
||||||
labelText?: React.ReactNode;
|
labelText?: React.ReactNode;
|
||||||
value?: string;
|
value?: string;
|
||||||
valid?: boolean
|
valid?: boolean;
|
||||||
inputClassName?: string;
|
inputClassName?: string;
|
||||||
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "id">;
|
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "id">;
|
||||||
|
|
||||||
|
const InputField = React.forwardRef<HTMLInputElement, Props>(({ onChange, htmlFor, labelText, children, valid = true, className, inputClassName, ...inputProps }, ref) => {
|
||||||
|
return (
|
||||||
|
<div spellCheck={false} className={`${className} ${valid ? "border-border" : "border-invalid"} h-11 inline-flex relative border px-3 w-full rounded-normal group focus-within:border`}>
|
||||||
|
<label className="absolute start-2 -top-2.5 px-2 text-[12px] text-foreground bg-container" htmlFor={htmlFor}>
|
||||||
|
{labelText}
|
||||||
|
</label>
|
||||||
|
|
||||||
const InputField = React.forwardRef<HTMLInputElement, Props>(
|
<Tooltip content={inputProps["aria-errormessage"]} hidden={!inputProps["aria-errormessage"]}>
|
||||||
({ onChange, htmlFor, labelText, children, valid = true, className, inputClassName, ...inputProps }, ref) => {
|
<input
|
||||||
return (
|
ref={ref}
|
||||||
<div
|
onChange={onChange}
|
||||||
spellCheck={false}
|
className={clsx(inputProps["aria-errormessage"] && "text-red-300!", "py-2.5 pt-3.5 text-sm2 w-full outline-0 leading-6!", inputClassName)}
|
||||||
className={`${className} ${valid ? 'border-border' : 'border-invalid'} h-11 inline-flex relative border px-3 w-full rounded-normal group focus-within:border`}>
|
name={htmlFor}
|
||||||
<label
|
{...inputProps}
|
||||||
className='absolute start-2 -top-2.5 px-2 text-[12px] text-foreground bg-container'
|
/>
|
||||||
htmlFor={htmlFor}>
|
{children}
|
||||||
{labelText}
|
</Tooltip>
|
||||||
</label>
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
<Tooltip content={inputProps['aria-errormessage']} hidden={!inputProps['aria-errormessage']}>
|
InputField.displayName = "InputField";
|
||||||
<input
|
|
||||||
ref={ref}
|
|
||||||
onChange={onChange}
|
|
||||||
className={clsx(
|
|
||||||
inputProps['aria-errormessage'] && 'text-red-300!',
|
|
||||||
'py-2.5 pt-3.5 text-sm2 w-full outline-0 leading-6!',
|
|
||||||
inputClassName
|
|
||||||
)}
|
|
||||||
name={htmlFor}
|
|
||||||
{...inputProps} />
|
|
||||||
{children}
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
InputField.displayName = 'InputField'
|
export default InputField;
|
||||||
|
|
||||||
export default InputField
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import SearchIcon from '../icons/SearchIcon'
|
import SearchIcon from '../icons/SearchIcon'
|
||||||
import clsx from 'clsx'
|
import { glassSurfaceFlat } from '@/lib/styles/glassSurface'
|
||||||
|
|
||||||
interface SearchboxProps
|
interface SearchboxProps
|
||||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'results'> {
|
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'results'> {
|
||||||
@@ -19,9 +19,9 @@ export default function SearchBox({
|
|||||||
}: SearchboxProps) {
|
}: SearchboxProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={glassSurfaceFlat(
|
||||||
'bg-container inline-flex rounded-xl px-4 h-10 w-full items-center content-center',
|
'inline-flex rounded-xl px-4 h-10 w-full items-center content-center',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SearchIcon stroke='#8C90A3' />
|
<SearchIcon stroke='#8C90A3' />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { glassSurface } from '@/lib/styles/glassSurface';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -8,7 +9,7 @@ type Props = {
|
|||||||
|
|
||||||
function CategoryItemRenderer({ children, ...rest }: Props) {
|
function CategoryItemRenderer({ children, ...rest }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className={`${rest.className} cursor-pointer transition-all duration-200 ease-out gradient-border overflow-hidden rounded-xl backdrop-blur-md bg-container/40`}>
|
<div className={glassSurface(rest.className, 'cursor-pointer transition-all duration-200 ease-out overflow-hidden rounded-xl')}>
|
||||||
<div
|
<div
|
||||||
{...rest}
|
{...rest}
|
||||||
className="rounded-normal w-[108px] h-[88px] flex flex-col justify-center items-center gap-2"
|
className="rounded-normal w-[108px] h-[88px] flex flex-col justify-center items-center gap-2"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { glassSurface } from '@/lib/styles/glassSurface';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -8,7 +9,7 @@ type Props = {
|
|||||||
|
|
||||||
function CategorySmallItemRenderer({ children, ...rest }: Props) {
|
function CategorySmallItemRenderer({ children, ...rest }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className={`${rest.className} cursor-pointer transition-all duration-200 ease-out gradient-border overflow-hidden rounded-xl backdrop-blur-md bg-container/40 dark:bg-background`}>
|
<div className={glassSurface(rest.className, 'cursor-pointer transition-all duration-200 ease-out overflow-hidden rounded-xl')}>
|
||||||
<div
|
<div
|
||||||
{...rest}
|
{...rest}
|
||||||
className="rounded-normal h-[44px] flex flex-row justify-center items-center p-2.5 gap-2"
|
className="rounded-normal h-[44px] flex flex-row justify-center items-center p-2.5 gap-2"
|
||||||
|
|||||||
@@ -131,26 +131,29 @@ const MenuItem = ({ food }: MenuItemProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="inline-flex mt-2 gap-2 justify-between w-full items-center">
|
<div className="inline-flex mt-2 gap-2 justify-between w-full items-start">
|
||||||
<div className="w-full flex flex-col gap-1" dir="ltr">
|
<div
|
||||||
|
className="w-full flex flex-col gap-1 items-end"
|
||||||
|
dir="ltr"
|
||||||
|
>
|
||||||
{hasDiscount ? (
|
{hasDiscount ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-xs text-disabled-text line-through">
|
<span className="text-xs text-disabled-text line-through">
|
||||||
{formattedOriginalPrice} T
|
{formattedOriginalPrice} T
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm font-medium text-primary dark:text-foreground">
|
<span className="text-sm font-medium text-black dark:text-white">
|
||||||
{formattedPrice} T
|
{formattedPrice} T
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm mt-1 text-right">
|
<span className="text-sm text-right">
|
||||||
{formattedPrice} T
|
{formattedPrice} T
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<motion.div
|
<motion.div
|
||||||
whileTap={isAvailable ? { scale: 1.05 } : undefined}
|
whileTap={isAvailable ? { scale: 1.05 } : undefined}
|
||||||
className="bg-background active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
|
className="bg-background pattern-secondary-bg active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
|
||||||
>
|
>
|
||||||
{quantity <= 0 ? (
|
{quantity <= 0 ? (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import React from 'react';
|
import { glassSurface } from "@/lib/styles/glassSurface";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -8,10 +9,7 @@ type Props = {
|
|||||||
|
|
||||||
function MenuItemRendererComponent({ children, ...rest }: Props) {
|
function MenuItemRendererComponent({ children, ...rest }: Props) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div {...rest} className={glassSurface(rest.className, "pb-4! py-4 transition-all duration-200 ease-out w-full rounded-3xl px-4 flex items-center justify-between gap-4")}>
|
||||||
{...rest}
|
|
||||||
className={`${rest.className} box-shadow-normal !pb-4 py-4 transition-all duration-200 ease-out w-full bg-container rounded-3xl px-4 flex items-center justify-between gap-4`}>
|
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||||
import { toast, toastLoginRequired } from '../Toast';
|
import { toast, toastLoginRequired } from '../Toast';
|
||||||
|
import { glassSurface } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
type MenuItemType = {
|
type MenuItemType = {
|
||||||
href: string | undefined;
|
href: string | undefined;
|
||||||
@@ -344,7 +345,7 @@ function SideMenu({ menuState, toggleMenuState, nightModeState, togglenightModeS
|
|||||||
</motion.nav>
|
</motion.nav>
|
||||||
</BlurredOverlayContainer>
|
</BlurredOverlayContainer>
|
||||||
<nav
|
<nav
|
||||||
className="hidden fixed top-4 bottom-4 right-4 bg-container xl:w-[250px] xl:flex flex-col z-40 overflow-clip xl:rounded-4xl"
|
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()}
|
{renderMenu()}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useCart } from '@/app/[name]/(Main)/cart/hook/useCart';
|
import { useCart } from '@/app/[name]/(Main)/cart/hook/useCart';
|
||||||
import { Building } from 'iconsax-react';
|
import { Building } from 'iconsax-react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { glassSurfaceNav } from '@/lib/styles/glassSurface';
|
||||||
|
import { getBottomNavMaskStyle } from './bottomNavShape';
|
||||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||||
import { toastLoginRequired } from '../Toast';
|
import { toastLoginRequired } from '../Toast';
|
||||||
|
|
||||||
@@ -37,6 +39,8 @@ function BottomNavBar({ onPagerClick }: BottomNavBarProps) {
|
|||||||
});
|
});
|
||||||
const { items } = useCart();
|
const { items } = useCart();
|
||||||
|
|
||||||
|
const navMaskStyle = React.useMemo(() => getBottomNavMaskStyle(), []);
|
||||||
|
|
||||||
const cartItemsCount = React.useMemo(() => {
|
const cartItemsCount = React.useMemo(() => {
|
||||||
return Object.values(items).reduce((total, item) => {
|
return Object.values(items).reduce((total, item) => {
|
||||||
return total + (item?.quantity || 0);
|
return total + (item?.quantity || 0);
|
||||||
@@ -56,119 +60,73 @@ function BottomNavBar({ onPagerClick }: BottomNavBarProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="fixed bottom-0 left-0 w-full bg-transparent pointer-events-none">
|
<div className="fixed bottom-0 left-0 w-full bg-transparent pointer-events-none">
|
||||||
<div className="max-w-md mx-auto w-full aspect-436/104 relative z-30">
|
<div className="max-w-md mx-auto w-full aspect-436/104 relative z-30">
|
||||||
<svg
|
<div
|
||||||
viewBox="0 0 436 104"
|
aria-hidden
|
||||||
fill="none"
|
className={glassSurfaceNav('absolute inset-0 pointer-events-none')}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
style={navMaskStyle}
|
||||||
className="w-full h-full block"
|
/>
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
|
<nav
|
||||||
|
className="absolute left-0 right-0 grid grid-cols-5 gap-x-1 items-end px-4 text-[10px] pointer-events-auto"
|
||||||
|
style={{
|
||||||
|
top: 'calc(10 / 104 * 100%)',
|
||||||
|
height: 'calc(80 / 104 * 100%)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<g filter="url(#filter0_d_9095_18036)" className='fill-container'>
|
<BottomNavLink
|
||||||
<path
|
href={`/${name}/cart`}
|
||||||
className='pointer-events-auto'
|
icon={<BagIcon width={20} height={20} />}
|
||||||
d="M218 16C229.034 16 238.711 22.0313 244.148 31.0943C245.844 33.9222 248.724 36 252.022 36H406C414.837 36 422 43.1634 422 52V80C422 88.8366 414.837 96 406 96H30C21.1634 96 14 88.8366 14 80V52C14 43.1634 21.1634 36 30 36H183.979C187.277 36 190.157 33.9222 191.853 31.0943C197.29 22.0315 206.966 16.0001 218 16Z"
|
value={t('Cart')}
|
||||||
/>
|
/>
|
||||||
</g>
|
{onPagerClick ? (
|
||||||
|
<button
|
||||||
{/* 🔽 Your HTML content inside SVG */}
|
onClick={onPagerClick}
|
||||||
<foreignObject x="0" y="10" width="100%" height="80">
|
className="flex flex-col justify-arround items-center gap-[5px] text-disabled-text pointer-events-auto **:stroke-disabled-text"
|
||||||
<nav
|
|
||||||
className="h-full px-4 grid grid-cols-5 gap-x-1 text-[10px] items-end"
|
|
||||||
>
|
>
|
||||||
<BottomNavLink
|
<PagerIcon width={20} height={20} />
|
||||||
href={`/${name}/cart`}
|
<span className="text-xs2">
|
||||||
icon={<BagIcon width={20} height={20} />}
|
{t('Pager')}
|
||||||
value={t('Cart')}
|
</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<BottomNavLink href={`/${name}/pager`} icon={<PagerIcon width={20} height={20} />} value={t('Pager')} />
|
||||||
|
)}
|
||||||
|
<BottomNavHighlightLink
|
||||||
|
href={`/${name}`}
|
||||||
|
icon={
|
||||||
|
<HomeIcon
|
||||||
|
className="transition-all duration-200 stroke-primary"
|
||||||
|
fill="white"
|
||||||
|
stroke="currentColor"
|
||||||
|
variant={isHomeRoute ? 'Bold' : 'Outline'}
|
||||||
|
width={20}
|
||||||
|
height={20} />}
|
||||||
|
value={t('Menu')} />
|
||||||
|
<Link
|
||||||
|
href={`/${name}/about`}
|
||||||
|
className={clsx(
|
||||||
|
'flex flex-col justify-arround items-center gap-[5px] pointer-events-auto min-w-0',
|
||||||
|
isAboutRoute
|
||||||
|
? 'text-primary **:stroke-primary'
|
||||||
|
: 'text-disabled-text **:stroke-disabled-text',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="shrink-0">
|
||||||
|
<Building
|
||||||
|
key={`building-${buildingVariant}-${isAboutRoute}`}
|
||||||
|
size={20}
|
||||||
|
variant={buildingVariant as 'Bold' | 'Outline'}
|
||||||
|
color="currentColor"
|
||||||
|
className='stroke-0'
|
||||||
/>
|
/>
|
||||||
{onPagerClick ? (
|
</div>
|
||||||
<button
|
<span className="text-xs2 text-center truncate w-full">
|
||||||
onClick={onPagerClick}
|
{t('about')}
|
||||||
className="flex flex-col justify-arround items-center gap-[5px] text-primary pointer-events-auto **:stroke-primary dark:text-white dark:**:stroke-white"
|
</span>
|
||||||
>
|
</Link>
|
||||||
<PagerIcon width={20} height={20} />
|
<BottomNavLink href={`/${name}/favorite`} onClick={handleFavoritesClick} icon={<HeartIcon width={20} height={20} />} value={t('Favorites')} />
|
||||||
<span className="text-xs2 dark:text-white">
|
</nav>
|
||||||
{t('Pager')}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<BottomNavLink href={`/${name}/pager`} icon={<PagerIcon width={20} height={20} />} value={t('Pager')} />
|
|
||||||
)}
|
|
||||||
<BottomNavHighlightLink
|
|
||||||
href={`/${name}`}
|
|
||||||
icon={
|
|
||||||
<HomeIcon
|
|
||||||
className={clsx(
|
|
||||||
'transition-all duration-200 stroke-primary dark:stroke-black! dark:text-white'
|
|
||||||
)}
|
|
||||||
fill="white"
|
|
||||||
stroke="currentColor"
|
|
||||||
variant={isHomeRoute ? 'Bold' : 'Outline'}
|
|
||||||
width={20}
|
|
||||||
height={20} />}
|
|
||||||
value={t('Menu')} />
|
|
||||||
<Link
|
|
||||||
href={`/${name}/about`}
|
|
||||||
className={clsx(
|
|
||||||
'flex flex-col justify-arround items-center gap-[5px] text-primary pointer-events-auto **:stroke-primary min-w-0 dark:text-white dark:**:stroke-white'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="shrink-0 text-primary dark:text-white">
|
|
||||||
<Building
|
|
||||||
key={`building-${buildingVariant}-${isAboutRoute}`}
|
|
||||||
size={20}
|
|
||||||
variant={buildingVariant as 'Bold' | 'Outline'}
|
|
||||||
color="currentColor"
|
|
||||||
className='stroke-0'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs2 text-center truncate w-full dark:text-white">
|
|
||||||
{t('about')}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
<BottomNavLink href={`/${name}/favorite`} onClick={handleFavoritesClick} icon={<HeartIcon width={20} height={20} />} value={t('Favorites')} />
|
|
||||||
</nav>
|
|
||||||
</foreignObject>
|
|
||||||
|
|
||||||
<defs>
|
|
||||||
<filter
|
|
||||||
id="filter0_d_9095_18036"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
filterUnits="userSpaceOnUse"
|
|
||||||
colorInterpolationFilters="sRGB"
|
|
||||||
>
|
|
||||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
|
||||||
<feColorMatrix
|
|
||||||
in="SourceAlpha"
|
|
||||||
type="matrix"
|
|
||||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
|
||||||
result="hardAlpha"
|
|
||||||
/>
|
|
||||||
<feOffset dy="-2" />
|
|
||||||
<feGaussianBlur stdDeviation="7" />
|
|
||||||
<feComposite in2="hardAlpha" operator="out" />
|
|
||||||
<feColorMatrix
|
|
||||||
type="matrix"
|
|
||||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"
|
|
||||||
/>
|
|
||||||
<feBlend
|
|
||||||
mode="normal"
|
|
||||||
in2="BackgroundImageFix"
|
|
||||||
result="effect1_dropShadow_9095_18036"
|
|
||||||
/>
|
|
||||||
<feBlend
|
|
||||||
mode="normal"
|
|
||||||
in="SourceGraphic"
|
|
||||||
in2="effect1_dropShadow_9095_18036"
|
|
||||||
result="shape"
|
|
||||||
/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
{/* Badge Layer - خارج از SVG - دقیقاً مثل foreignObject */}
|
|
||||||
{cartItemsCount > 0 && (
|
{cartItemsCount > 0 && (
|
||||||
<div
|
<div
|
||||||
className="absolute left-0 right-0 pointer-events-none"
|
className="absolute left-0 right-0 pointer-events-none"
|
||||||
|
|||||||
@@ -18,16 +18,16 @@ function BottomNavLink({ icon, value, href, prefetch, ...restProps }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Link {...restProps} href={href} prefetch={shouldPrefetch} className={clsx(
|
<Link {...restProps} href={href} prefetch={shouldPrefetch} className={clsx(
|
||||||
'flex flex-col justify-arround items-center gap-[5px] text-primary pointer-events-auto **:stroke-primary min-w-0 dark:text-white dark:**:stroke-white',
|
'flex flex-col justify-arround items-center gap-[5px] pointer-events-auto min-w-0',
|
||||||
isActive && 'text-primary-foreground **:stroke-primary-foreground',
|
isActive
|
||||||
|
? 'text-primary **:stroke-primary'
|
||||||
|
: 'text-disabled-text **:stroke-disabled-text',
|
||||||
restProps.className ?? '',
|
restProps.className ?? '',
|
||||||
)}>
|
)}>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<span className={clsx(
|
<span className="text-xs2 text-center truncate w-full">
|
||||||
'text-xs2 text-center truncate w-full dark:text-white',
|
|
||||||
)}>
|
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import type { LinkProps } from 'next/link'
|
import type { LinkProps } from 'next/link'
|
||||||
import { usePathname } from 'next/navigation'
|
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -12,9 +11,6 @@ type Props = {
|
|||||||
} & LinkProps & React.AnchorHTMLAttributes<HTMLAnchorElement>
|
} & LinkProps & React.AnchorHTMLAttributes<HTMLAnchorElement>
|
||||||
|
|
||||||
function BottomNavHighlightLink({ icon, value, href, ...restProps }: Props) {
|
function BottomNavHighlightLink({ icon, value, href, ...restProps }: Props) {
|
||||||
const pathname = usePathname();
|
|
||||||
const isActive = pathname === href;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link {...restProps} href={href} className={clsx(
|
<Link {...restProps} href={href} className={clsx(
|
||||||
'flex flex-col justify-arround items-center gap-[5px] text-primary pointer-events-auto',
|
'flex flex-col justify-arround items-center gap-[5px] text-primary pointer-events-auto',
|
||||||
@@ -25,7 +21,7 @@ function BottomNavHighlightLink({ icon, value, href, ...restProps }: Props) {
|
|||||||
)}>
|
)}>
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs2 dark:text-white">
|
<span className="text-xs2 text-disabled-text">
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { CSSProperties } from "react";
|
||||||
|
|
||||||
|
export const BOTTOM_NAV_SHAPE_PATH =
|
||||||
|
"M218 16C229.034 16 238.711 22.0313 244.148 31.0943C245.844 33.9222 248.724 36 252.022 36H406C414.837 36 422 43.1634 422 52V80C422 88.8366 414.837 96 406 96H30C21.1634 96 14 88.8366 14 80V52C14 43.1634 21.1634 36 30 36H183.979C187.277 36 190.157 33.9222 191.853 31.0943C197.29 22.0315 206.966 16.0001 218 16Z";
|
||||||
|
|
||||||
|
export const BOTTOM_NAV_VIEWBOX = "0 0 436 104";
|
||||||
|
|
||||||
|
export function getBottomNavMaskStyle(): CSSProperties {
|
||||||
|
const maskSvg = `<svg viewBox="${BOTTOM_NAV_VIEWBOX}" xmlns="http://www.w3.org/2000/svg"><path fill="white" d="${BOTTOM_NAV_SHAPE_PATH}"/></svg>`;
|
||||||
|
const maskUrl = `url("data:image/svg+xml,${encodeURIComponent(maskSvg)}")`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
WebkitMaskImage: maskUrl,
|
||||||
|
maskImage: maskUrl,
|
||||||
|
WebkitMaskSize: "100% 100%",
|
||||||
|
maskSize: "100% 100%",
|
||||||
|
WebkitMaskRepeat: "no-repeat",
|
||||||
|
maskRepeat: "no-repeat",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useLayoutEffect, useState } from 'react'
|
||||||
import PingAnimation from './animations/PingAnimation';
|
import PingAnimation from './animations/PingAnimation';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -10,21 +10,23 @@ type Props = {
|
|||||||
} & React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
|
} & React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
|
||||||
|
|
||||||
const LoadingOverlay = ({ visible = true, bgOpacity = 30, delay = 0, children = <PingAnimation /> }: Props) => {
|
const LoadingOverlay = ({ visible = true, bgOpacity = 30, delay = 0, children = <PingAnimation /> }: Props) => {
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(visible);
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
// Trigger fade-in after mount
|
setLoaded(visible);
|
||||||
const timeout = setTimeout(() => setLoaded(visible), 300);
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-visible={visible}
|
data-visible={visible}
|
||||||
data-loaded={loaded}
|
data-loaded={loaded}
|
||||||
className={`absolute inset-0 flex items-center justify-center
|
className={`absolute inset-0 flex items-center justify-center
|
||||||
backdrop-blur-sm bg-background/${bgOpacity}
|
backdrop-blur-sm bg-background/${bgOpacity}
|
||||||
opacity-0 transition-opacity duration-300 ease-in-out delay-${delay}
|
transition-opacity duration-300 ease-in-out delay-${delay}
|
||||||
data-[loaded=false]:-z-50
|
data-[loaded=false]:-z-50
|
||||||
data-[visible=true]:z-50
|
data-[visible=true]:z-50
|
||||||
data-[visible=true]:opacity-100`}
|
data-[visible=true]:opacity-100`}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { motion } from "framer-motion";
|
||||||
import { motion } from 'framer-motion';
|
import Image from "next/image";
|
||||||
import Image from 'next/image';
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
const MAX_DISPLAY_MS = 1200;
|
const MAX_DISPLAY_MS = 1200;
|
||||||
const EXIT_MS = 350;
|
const EXIT_MS = 350;
|
||||||
@@ -14,27 +14,21 @@ interface SplashScreenProps {
|
|||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SplashScreen({
|
export default function SplashScreen({ logo, restaurantName, onDismiss }: SplashScreenProps) {
|
||||||
logo,
|
|
||||||
restaurantName,
|
|
||||||
onDismiss,
|
|
||||||
}: SplashScreenProps) {
|
|
||||||
const [opacity, setOpacity] = useState(1);
|
const [opacity, setOpacity] = useState(1);
|
||||||
const [logoState, setLogoState] = useState<'idle' | 'loading' | 'ready' | 'error'>(
|
const [logoState, setLogoState] = useState<"idle" | "loading" | "ready" | "error">(logo ? "loading" : "idle");
|
||||||
logo ? 'loading' : 'idle',
|
|
||||||
);
|
|
||||||
const onDismissRef = useRef(onDismiss);
|
const onDismissRef = useRef(onDismiss);
|
||||||
onDismissRef.current = onDismiss;
|
onDismissRef.current = onDismiss;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLogoState(logo ? 'loading' : 'idle');
|
setLogoState(logo ? "loading" : "idle");
|
||||||
}, [logo]);
|
}, [logo]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!logo || logoState !== 'loading') return;
|
if (!logo || logoState !== "loading") return;
|
||||||
|
|
||||||
const logoTimeout = setTimeout(() => {
|
const logoTimeout = setTimeout(() => {
|
||||||
setLogoState('error');
|
setLogoState("error");
|
||||||
}, LOGO_TIMEOUT_MS);
|
}, LOGO_TIMEOUT_MS);
|
||||||
|
|
||||||
return () => clearTimeout(logoTimeout);
|
return () => clearTimeout(logoTimeout);
|
||||||
@@ -55,14 +49,13 @@ export default function SplashScreen({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const showLogoSpinner = Boolean(logo) && logoState === 'loading';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 1 }}
|
initial={{ opacity: 1 }}
|
||||||
animate={{ opacity }}
|
animate={{ opacity }}
|
||||||
transition={{ duration: EXIT_MS / 1000, ease: 'easeOut' }}
|
transition={{ duration: EXIT_MS / 1000, ease: "easeOut" }}
|
||||||
className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-background"
|
className="fixed inset-0 z-9999 flex flex-col items-center justify-center bg-background"
|
||||||
|
data-slot="splash-screen"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
@@ -74,7 +67,7 @@ export default function SplashScreen({
|
|||||||
transition={{
|
transition={{
|
||||||
duration: 3,
|
duration: 3,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
ease: 'easeInOut',
|
ease: "easeInOut",
|
||||||
}}
|
}}
|
||||||
className="absolute -top-20 -right-20 w-80 h-80 rounded-full bg-primary/20 blur-3xl"
|
className="absolute -top-20 -right-20 w-80 h-80 rounded-full bg-primary/20 blur-3xl"
|
||||||
/>
|
/>
|
||||||
@@ -87,35 +80,30 @@ export default function SplashScreen({
|
|||||||
duration: 3,
|
duration: 3,
|
||||||
delay: 0.5,
|
delay: 0.5,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
ease: 'easeInOut',
|
ease: "easeInOut",
|
||||||
}}
|
}}
|
||||||
className="absolute -bottom-20 -left-20 w-80 h-80 rounded-full bg-primary/20 blur-3xl"
|
className="absolute -bottom-20 -left-20 w-80 h-80 rounded-full bg-primary/20 blur-3xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-8 px-8 w-full max-w-md relative z-10">
|
<div className="flex flex-col items-center gap-8 px-8 w-full max-w-md relative z-10">
|
||||||
{logo && logoState !== 'error' && (
|
{logo && logoState !== "error" && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Image
|
<Image
|
||||||
src={logo}
|
src={logo}
|
||||||
alt={restaurantName || 'Restaurant Logo'}
|
alt={restaurantName || "Restaurant Logo"}
|
||||||
width={200}
|
width={200}
|
||||||
height={100}
|
height={100}
|
||||||
unoptimized
|
unoptimized
|
||||||
priority
|
priority
|
||||||
className="object-contain"
|
className="object-contain"
|
||||||
onLoad={() => setLogoState('ready')}
|
onLoad={() => setLogoState("ready")}
|
||||||
onError={() => setLogoState('error')}
|
onError={() => setLogoState("error")}
|
||||||
style={{
|
style={{
|
||||||
opacity: logoState === 'ready' ? 1 : 0,
|
opacity: logoState === "ready" ? 1 : 0,
|
||||||
transition: 'opacity 0.3s',
|
transition: "opacity 0.3s",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{showLogoSpinner && (
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<div className="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -131,7 +119,7 @@ export default function SplashScreen({
|
|||||||
duration: 1.5,
|
duration: 1.5,
|
||||||
delay,
|
delay,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
ease: 'easeInOut',
|
ease: "easeInOut",
|
||||||
}}
|
}}
|
||||||
className="w-3 h-3 rounded-full bg-primary shadow-lg shadow-primary/50"
|
className="w-3 h-3 rounded-full bg-primary shadow-lg shadow-primary/50"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { TabHeader, TabItemProps } from './TabHeader';
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import TabHeaderRenderer from './TabHeaderRenderer';
|
import TabHeaderRenderer from './TabHeaderRenderer';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import { glassSurfaceNav } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
itemRenderer?: (child: ReactNode, index: number) => ReactElement;
|
itemRenderer?: (child: ReactNode, index: number) => ReactElement;
|
||||||
@@ -38,7 +39,7 @@ export type TabContainerClassName = {
|
|||||||
|
|
||||||
export const TabContainerClassNames: TabContainerClassName = {
|
export const TabContainerClassNames: TabContainerClassName = {
|
||||||
wrapper: 'h-full',
|
wrapper: 'h-full',
|
||||||
scrollView: 'h-[72px] gradient-border !pt-3 !pb-2 w-full overflow-y-hidden bg-[#FFFFFF70] dark:bg-container/70 justify-center rounded-[32px] inline-flex items-center gap-10',
|
scrollView: glassSurfaceNav('h-[72px] gradient-border !pt-3 !pb-2 w-full overflow-y-hidden justify-center rounded-[32px] inline-flex items-center gap-10'),
|
||||||
header: '',
|
header: '',
|
||||||
headerDeactive: '',
|
headerDeactive: '',
|
||||||
headerActive: '',
|
headerActive: '',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { usePathname, useRouter } from 'next/navigation';
|
|||||||
import usePreference from '@/hooks/helpers/usePreference';
|
import usePreference from '@/hooks/helpers/usePreference';
|
||||||
import PagerModal from '../pager/PagerModal';
|
import PagerModal from '../pager/PagerModal';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { glassSurface } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
type Props = {} & React.AllHTMLAttributes<HTMLBodyElementEventMap>
|
type Props = {} & React.AllHTMLAttributes<HTMLBodyElementEventMap>
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ function ClientMenuRouteWrapper({ children }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="h-svh overflow-hidden pt-10 flex flex-col pb-4 xl:pt-12">
|
<div className="h-svh overflow-hidden pt-10 flex flex-col pb-4 xl:pt-12">
|
||||||
|
|
||||||
<div className="z-50 fixed right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-4 bg-container justify-between rounded-[32px] xl:w-[calc(100%-305px)]">
|
<div 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)]')}>
|
||||||
<TopBar
|
<TopBar
|
||||||
profileDropState={profileDrop}
|
profileDropState={profileDrop}
|
||||||
toggleProfileDropState={toggleProfileDrop}
|
toggleProfileDropState={toggleProfileDrop}
|
||||||
|
|||||||
@@ -1,20 +1,53 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import usePreference from '@/hooks/helpers/usePreference';
|
import usePreference from '@/hooks/helpers/usePreference';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
|
import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
|
||||||
import SplashScreen from '@/components/overlays/SplashScreen';
|
import SplashScreen from '@/components/overlays/SplashScreen';
|
||||||
import { hexToOklch, calculateBrightness } from '@/lib/helpers/colorUtils';
|
import { hexToOklch, calculateBrightness } from '@/lib/helpers/colorUtils';
|
||||||
|
import {
|
||||||
|
applyCachedThemeToDom,
|
||||||
|
buildThemeCacheFromRestaurant,
|
||||||
|
clearRestaurantBackgroundOverrides,
|
||||||
|
getCachedMenuColor,
|
||||||
|
getThemeCache,
|
||||||
|
setThemeCache,
|
||||||
|
} from '@/lib/helpers/themeCache';
|
||||||
|
|
||||||
function getSplashStorageKey(restaurantSlug: string) {
|
function getSplashStorageKey(restaurantSlug: string) {
|
||||||
return `splash-seen:${restaurantSlug}`;
|
return `splash-seen:${restaurantSlug}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSplashSessionKey(restaurantSlug: string) {
|
||||||
|
return `splash-session:${restaurantSlug}`;
|
||||||
|
}
|
||||||
|
|
||||||
function getSplashFingerprint(logo?: string | null, name?: string | null) {
|
function getSplashFingerprint(logo?: string | null, name?: string | null) {
|
||||||
return `${logo ?? ''}|${name ?? ''}`;
|
return `${logo ?? ''}|${name ?? ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readStoredRestaurantIdentity() {
|
||||||
|
return {
|
||||||
|
logo: localStorage.getItem('restaurant-logo'),
|
||||||
|
name: localStorage.getItem('restaurant-name'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSeenSplashFingerprint(restaurantSlug: string, currentFingerprint: string) {
|
||||||
|
const storageKey = getSplashStorageKey(restaurantSlug);
|
||||||
|
const seenFingerprint = localStorage.getItem(storageKey);
|
||||||
|
|
||||||
|
if (!seenFingerprint || seenFingerprint === '|') {
|
||||||
|
if (currentFingerprint !== '|') {
|
||||||
|
localStorage.setItem(storageKey, currentFingerprint);
|
||||||
|
return currentFingerprint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return seenFingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
@@ -43,73 +76,113 @@ function PreferenceWrapper({ children }: Props) {
|
|||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const [cachedLogo, setCachedLogo] = useState<string | null>(null);
|
const [cachedLogo, setCachedLogo] = useState<string | null>(null);
|
||||||
const [cachedName, setCachedName] = useState<string | null>(null);
|
const [cachedName, setCachedName] = useState<string | null>(null);
|
||||||
|
const splashCheckedRef = useRef(false);
|
||||||
|
|
||||||
const handleSplashDismiss = useCallback(() => {
|
const handleSplashDismiss = useCallback(() => {
|
||||||
setShowSplash(false);
|
setShowSplash(false);
|
||||||
|
|
||||||
if (!restaurantSlug) return;
|
if (!restaurantSlug) return;
|
||||||
|
|
||||||
const logo = aboutData?.data?.logo || cachedLogo;
|
const stored = readStoredRestaurantIdentity();
|
||||||
const name = aboutData?.data?.name || cachedName;
|
const logo = aboutData?.data?.logo || stored.logo || cachedLogo;
|
||||||
|
const name = aboutData?.data?.name || stored.name || cachedName;
|
||||||
const fingerprint = getSplashFingerprint(logo, name);
|
const fingerprint = getSplashFingerprint(logo, name);
|
||||||
|
|
||||||
localStorage.setItem(getSplashStorageKey(restaurantSlug), fingerprint);
|
localStorage.setItem(getSplashStorageKey(restaurantSlug), fingerprint);
|
||||||
|
sessionStorage.setItem(getSplashSessionKey(restaurantSlug), '1');
|
||||||
}, [aboutData?.data?.logo, aboutData?.data?.name, cachedLogo, cachedName, restaurantSlug]);
|
}, [aboutData?.data?.logo, aboutData?.data?.name, cachedLogo, cachedName, restaurantSlug]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (splashCheckedRef.current) return;
|
||||||
|
splashCheckedRef.current = true;
|
||||||
|
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
|
|
||||||
const savedColor = localStorage.getItem('theme-primary-color');
|
const savedColor = getCachedMenuColor(restaurantSlug);
|
||||||
if (savedColor) {
|
const cachedTheme = restaurantSlug ? getThemeCache(restaurantSlug) : null;
|
||||||
|
if (cachedTheme) {
|
||||||
|
applyCachedThemeToDom(cachedTheme);
|
||||||
|
} else if (savedColor) {
|
||||||
applyPrimaryColor(savedColor);
|
applyPrimaryColor(savedColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedLogo = localStorage.getItem('restaurant-logo');
|
const { logo: savedLogo, name: savedName } = readStoredRestaurantIdentity();
|
||||||
const savedName = localStorage.getItem('restaurant-name');
|
|
||||||
if (savedLogo) setCachedLogo(savedLogo);
|
if (savedLogo) setCachedLogo(savedLogo);
|
||||||
if (savedName) setCachedName(savedName);
|
if (savedName) setCachedName(savedName);
|
||||||
|
|
||||||
if (!restaurantSlug) return;
|
if (!restaurantSlug) return;
|
||||||
|
|
||||||
|
if (sessionStorage.getItem(getSplashSessionKey(restaurantSlug))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentFingerprint = getSplashFingerprint(savedLogo, savedName);
|
const currentFingerprint = getSplashFingerprint(savedLogo, savedName);
|
||||||
const seenFingerprint = localStorage.getItem(getSplashStorageKey(restaurantSlug));
|
const seenFingerprint = resolveSeenSplashFingerprint(restaurantSlug, currentFingerprint);
|
||||||
|
|
||||||
if (seenFingerprint !== currentFingerprint) {
|
if (seenFingerprint !== currentFingerprint) {
|
||||||
setShowSplash(true);
|
setShowSplash(true);
|
||||||
|
sessionStorage.setItem(getSplashSessionKey(restaurantSlug), '1');
|
||||||
}
|
}
|
||||||
}, [restaurantSlug]);
|
}, [restaurantSlug]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (aboutData?.data?.menuColor) {
|
const menuColor = aboutData?.data?.menuColor;
|
||||||
localStorage.setItem('theme-primary-color', aboutData.data.menuColor);
|
if (!menuColor || !restaurantSlug) return;
|
||||||
|
|
||||||
if (aboutData.data.logo) {
|
const restaurant = aboutData.data;
|
||||||
localStorage.setItem('restaurant-logo', aboutData.data.logo);
|
const cached = getThemeCache(restaurantSlug);
|
||||||
}
|
|
||||||
if (aboutData.data.name) {
|
|
||||||
localStorage.setItem('restaurant-name', aboutData.data.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
applyPrimaryColor(aboutData.data.menuColor);
|
const themeBase = buildThemeCacheFromRestaurant(restaurant);
|
||||||
|
setThemeCache(restaurantSlug, {
|
||||||
|
menuColor,
|
||||||
|
bgType: themeBase.bgType,
|
||||||
|
bgUrl: restaurant.bgUrl,
|
||||||
|
bgBlur: themeBase.bgBlur,
|
||||||
|
bgOpacity: themeBase.bgOpacity,
|
||||||
|
bgOverlay: themeBase.bgOverlay,
|
||||||
|
isPattern: themeBase.isPattern,
|
||||||
|
patternDataUrl: cached?.patternDataUrl ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedTheme = getThemeCache(restaurantSlug);
|
||||||
|
if (updatedTheme) {
|
||||||
|
applyCachedThemeToDom(updatedTheme);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [aboutData?.data?.menuColor]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (restaurant.logo) {
|
||||||
if (!restaurantSlug || showSplash) return;
|
localStorage.setItem('restaurant-logo', restaurant.logo);
|
||||||
|
}
|
||||||
|
if (restaurant.name) {
|
||||||
|
localStorage.setItem('restaurant-name', restaurant.name);
|
||||||
|
}
|
||||||
|
|
||||||
const logo = aboutData?.data?.logo || cachedLogo;
|
applyPrimaryColor(menuColor);
|
||||||
const name = aboutData?.data?.name || cachedName;
|
|
||||||
const fingerprint = getSplashFingerprint(logo, name);
|
const currentFingerprint = getSplashFingerprint(
|
||||||
|
restaurant.logo ?? readStoredRestaurantIdentity().logo,
|
||||||
|
restaurant.name ?? readStoredRestaurantIdentity().name,
|
||||||
|
);
|
||||||
const seenFingerprint = localStorage.getItem(getSplashStorageKey(restaurantSlug));
|
const seenFingerprint = localStorage.getItem(getSplashStorageKey(restaurantSlug));
|
||||||
|
if (seenFingerprint === '|' && currentFingerprint !== '|') {
|
||||||
if (seenFingerprint === '|' && fingerprint !== '|') {
|
localStorage.setItem(getSplashStorageKey(restaurantSlug), currentFingerprint);
|
||||||
localStorage.setItem(getSplashStorageKey(restaurantSlug), fingerprint);
|
|
||||||
}
|
}
|
||||||
}, [aboutData?.data?.logo, aboutData?.data?.name, cachedLogo, cachedName, restaurantSlug, showSplash]);
|
}, [aboutData?.data, restaurantSlug]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute('data-theme', nightMode ? 'dark' : 'light');
|
document.documentElement.setAttribute('data-theme', nightMode ? 'dark' : 'light');
|
||||||
}, [nightMode]);
|
|
||||||
|
if (nightMode) {
|
||||||
|
clearRestaurantBackgroundOverrides();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!restaurantSlug) return;
|
||||||
|
|
||||||
|
const cached = getThemeCache(restaurantSlug);
|
||||||
|
if (cached) {
|
||||||
|
applyCachedThemeToDom(cached);
|
||||||
|
}
|
||||||
|
}, [nightMode, restaurantSlug]);
|
||||||
|
|
||||||
const splashLogo = mounted
|
const splashLogo = mounted
|
||||||
? (aboutData?.data?.logo || cachedLogo || undefined)
|
? (aboutData?.data?.logo || cachedLogo || undefined)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
// import LoadingOverlay from '@/components/overlays/LoadingOverlay';
|
// import LoadingOverlay from '@/components/overlays/LoadingOverlay';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { glassSurfaceCard } from '@/lib/styles/glassSurface';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isPending: boolean,
|
isPending: boolean,
|
||||||
@@ -24,7 +25,7 @@ function AuthFormWrapper({ children, isPending, onSubmit, ...restProps }: Props)
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className='p-6 lg:py-[75px] w-full flex items-center justify-center py-4 lg:items-center lg:px-10 px-4 drop-shadow-black h-full'
|
className='p-6 lg:py-[75px] w-full flex items-center justify-center py-4 lg:items-center lg:px-10 px-4 drop-shadow-black h-full'
|
||||||
>
|
>
|
||||||
<div className='relative h-full w-full px-4 sm:px-6 lg:p-0 flex flex-col max-h-[812px] max-w-[1200px] bg-container rounded-container overflow-clip lg:flex-row justify-between'>
|
<div className={glassSurfaceCard('relative h-full w-full px-4 sm:px-6 lg:p-0 flex flex-col max-h-[812px] max-w-[1200px] overflow-clip lg:flex-row justify-between')}>
|
||||||
{children}
|
{children}
|
||||||
{/* <LoadingOverlay visible={isPending} bgOpacity={0} /> */}
|
{/* <LoadingOverlay visible={isPending} bgOpacity={0} /> */}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,126 +1,112 @@
|
|||||||
'use client'
|
"use client";
|
||||||
|
|
||||||
import Button from '@/components/button/PrimaryButton'
|
import { useOtpRequest } from "@/app/auth/login/hooks/useAuthData";
|
||||||
import InputField from '@/components/input/InputField'
|
import { LoginOTPRequestType } from "@/app/auth/login/types/Types";
|
||||||
import { AUTH_PAGE_ELEMENT, AUTH_STEP } from '@/enums'
|
import Button from "@/components/button/PrimaryButton";
|
||||||
import { LoginOTPRequestType } from '@/app/auth/login/types/Types'
|
import InputField from "@/components/input/InputField";
|
||||||
import Image from 'next/image'
|
import { toast } from "@/components/Toast";
|
||||||
import React, { useState } from 'react'
|
import { AUTH_PAGE_ELEMENT, AUTH_STEP } from "@/enums";
|
||||||
import type { ChangeEvent } from 'react'
|
import { useCountdown } from "@/hooks/useCountdown";
|
||||||
import { useTranslation } from 'react-i18next'
|
import { extractErrorMessage } from "@/lib/func";
|
||||||
import { useFormik } from 'formik'
|
import { glassSurfaceFlat } from "@/lib/styles/glassSurface";
|
||||||
import * as Yup from 'yup'
|
import { useFormik } from "formik";
|
||||||
import { useOtpRequest } from '@/app/auth/login/hooks/useAuthData'
|
import Image from "next/image";
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from "next/navigation";
|
||||||
import { toast } from '@/components/Toast'
|
import type { ChangeEvent } from "react";
|
||||||
import StepOtp from './StepOtp'
|
import { useState } from "react";
|
||||||
import { extractErrorMessage } from '@/lib/func'
|
import { useTranslation } from "react-i18next";
|
||||||
import { useCountdown } from '@/hooks/useCountdown'
|
import * as Yup from "yup";
|
||||||
|
import StepOtp from "./StepOtp";
|
||||||
|
|
||||||
type StepEnterNumberProps = {
|
type StepEnterNumberProps = {
|
||||||
pending?: boolean
|
pending?: boolean;
|
||||||
onChange?: (event: ChangeEvent<HTMLInputElement>) => void
|
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||||
value?: string
|
value?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
function StepEnterNumber(_props: StepEnterNumberProps = {}) {
|
function StepEnterNumber(_props: StepEnterNumberProps = {}) {
|
||||||
void _props
|
void _props;
|
||||||
|
|
||||||
const { t } = useTranslation('auth')
|
|
||||||
const { name } = useParams()
|
|
||||||
const [step, setStep] = useState(AUTH_STEP.ENTER_NUMBER)
|
|
||||||
const { mutate: mutateOtpRequest, isPending } = useOtpRequest()
|
|
||||||
const { timerRunning, secondsLeft, restart } = useCountdown(
|
|
||||||
step === AUTH_STEP.ENTER_OTP,
|
|
||||||
120
|
|
||||||
)
|
|
||||||
|
|
||||||
|
const { t } = useTranslation("auth");
|
||||||
|
const { name } = useParams();
|
||||||
|
const [step, setStep] = useState(AUTH_STEP.ENTER_NUMBER);
|
||||||
|
const { mutate: mutateOtpRequest, isPending } = useOtpRequest();
|
||||||
|
const { timerRunning, secondsLeft, restart } = useCountdown(step === AUTH_STEP.ENTER_OTP, 120);
|
||||||
|
|
||||||
const formik = useFormik<LoginOTPRequestType>({
|
const formik = useFormik<LoginOTPRequestType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
phone: '',
|
phone: "",
|
||||||
slug: '',
|
slug: "",
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
phone: Yup.string().required('Errors.PhoneNumberRequired').matches(/^09\d{9}$/, 'Errors.PhoneNumberFormat'),
|
phone: Yup.string()
|
||||||
|
.required("Errors.PhoneNumberRequired")
|
||||||
|
.matches(/^09\d{9}$/, "Errors.PhoneNumberFormat"),
|
||||||
}),
|
}),
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
if (name) {
|
if (name) {
|
||||||
values.slug = name as string
|
values.slug = name as string;
|
||||||
mutateOtpRequest(values, {
|
mutateOtpRequest(values, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast(t('otp_sent'), 'success')
|
toast(t("otp_sent"), "success");
|
||||||
setStep(AUTH_STEP.ENTER_OTP)
|
setStep(AUTH_STEP.ENTER_OTP);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast(extractErrorMessage(error), 'error')
|
toast(extractErrorMessage(error), "error");
|
||||||
|
},
|
||||||
}
|
});
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
const handleResendOtp = () => {
|
const handleResendOtp = () => {
|
||||||
if (timerRunning || !name) return
|
if (timerRunning || !name) return;
|
||||||
|
|
||||||
mutateOtpRequest(
|
mutateOtpRequest(
|
||||||
{ phone: formik.values.phone, slug: name as string },
|
{ phone: formik.values.phone, slug: name as string },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast(t('otp_sent'), 'success')
|
toast(t("otp_sent"), "success");
|
||||||
restart()
|
restart();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast(extractErrorMessage(error), 'error')
|
toast(extractErrorMessage(error), "error");
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Image
|
<Image className="object-cover w-full h-full max-h-1/2 lg:max-h-full lg:w-1/2" src={"/assets/images/login-banner.png"} alt="login banner" width={100} height={100} unoptimized priority />
|
||||||
className='object-cover w-full h-full max-h-1/2 lg:max-h-full lg:w-1/2'
|
<div className="w-full min-w-1/2 lg:max-w-1/2 h-full lg:px-9 py-7 flex flex-col justify-between lg:justify-center lg:gap-10">
|
||||||
src={'/assets/images/login-banner.png'}
|
<div className="w-full">
|
||||||
alt='login banner'
|
<div className="pt-4">
|
||||||
width={100}
|
{step === AUTH_STEP.ENTER_NUMBER && (
|
||||||
height={100}
|
<>
|
||||||
unoptimized
|
<h6 className="text-lg font-bold">{t("Enter.Heading")}</h6>
|
||||||
priority
|
<p className="mt-3 text-[13px]">{t("Enter.Description")}</p>
|
||||||
/>
|
</>
|
||||||
<div className='w-full min-w-1/2 lg:max-w-1/2 h-full lg:px-9 py-7 flex flex-col justify-between lg:justify-center lg:gap-10'>
|
)}
|
||||||
<div className='w-full'>
|
{step === AUTH_STEP.ENTER_OTP && (
|
||||||
<div className='pt-4'>
|
<>
|
||||||
{
|
<h6 className="text-lg font-bold">{t("OTP.Heading")}</h6>
|
||||||
step === AUTH_STEP.ENTER_NUMBER && (
|
<p className="mt-3 text-[13px]">{t("OTP.Description", { phoneNumber: formik.values.phone })}</p>
|
||||||
<>
|
</>
|
||||||
<h6 className='text-lg font-bold'>{t('Enter.Heading')}</h6>
|
)}
|
||||||
<p className='mt-3 text-[13px]'>{t('Enter.Description')}</p>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
step === AUTH_STEP.ENTER_OTP && (
|
|
||||||
<>
|
|
||||||
<h6 className='text-lg font-bold'>{t('OTP.Heading')}</h6>
|
|
||||||
<p className='mt-3 text-[13px]'>{t('OTP.Description', { phoneNumber: formik.values.phone })}</p>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
{step === AUTH_STEP.ENTER_NUMBER && (
|
{step === AUTH_STEP.ENTER_NUMBER && (
|
||||||
<InputField
|
<InputField
|
||||||
type='tel'
|
type="tel"
|
||||||
inputMode='tel'
|
inputMode="tel"
|
||||||
autoComplete='tel'
|
autoComplete="tel"
|
||||||
dir='ltr'
|
dir="ltr"
|
||||||
className='mt-10 w-full bg-container'
|
className={glassSurfaceFlat("mt-10 w-full")}
|
||||||
inputClassName='text-left dltr'
|
inputClassName="text-left dltr"
|
||||||
htmlFor={AUTH_PAGE_ELEMENT.INPUT_PHONE}
|
htmlFor={AUTH_PAGE_ELEMENT.INPUT_PHONE}
|
||||||
labelText={t('Enter.LabelPhoneNumber')}
|
labelText={t("Enter.LabelPhoneNumber")}
|
||||||
{...formik.getFieldProps('phone')}
|
{...formik.getFieldProps("phone")}
|
||||||
placeholder='09120000000'
|
placeholder="09120000000"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{step === AUTH_STEP.ENTER_OTP && (
|
{step === AUTH_STEP.ENTER_OTP && (
|
||||||
@@ -134,22 +120,14 @@ function StepEnterNumber(_props: StepEnterNumberProps = {}) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{
|
{step === AUTH_STEP.ENTER_NUMBER && (
|
||||||
step === AUTH_STEP.ENTER_NUMBER && (
|
<Button disabled={!formik.isValid} pending={isPending} type="button" onClick={() => formik.handleSubmit()} className="dark:bg-white dark:text-black hover:dark:bg-white">
|
||||||
<Button
|
{t("Enter.ButtonSubmit")}
|
||||||
disabled={!formik.isValid}
|
</Button>
|
||||||
pending={isPending}
|
)}
|
||||||
type='button'
|
|
||||||
onClick={() => formik.handleSubmit()}
|
|
||||||
className='dark:bg-white dark:text-black hover:dark:bg-white'
|
|
||||||
>
|
|
||||||
{t('Enter.ButtonSubmit')}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default StepEnterNumber
|
export default StepEnterNumber;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Image from 'next/image'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { glassSurfaceFlat } from '@/lib/styles/glassSurface'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onChange: ((e: React.ChangeEvent<HTMLInputElement>) => void) &
|
onChange: ((e: React.ChangeEvent<HTMLInputElement>) => void) &
|
||||||
@@ -68,7 +69,7 @@ function StepEnterPassword ({
|
|||||||
aria-errormessage={error}
|
aria-errormessage={error}
|
||||||
autoFocus
|
autoFocus
|
||||||
autoComplete='current-password'
|
autoComplete='current-password'
|
||||||
className='mt-10 bg-container'
|
className={glassSurfaceFlat('mt-10')}
|
||||||
htmlFor={AUTH_PAGE_ELEMENT.INPUT_PASSWORD}
|
htmlFor={AUTH_PAGE_ELEMENT.INPUT_PASSWORD}
|
||||||
labelText={t('EnterPass.LabelPass')}
|
labelText={t('EnterPass.LabelPass')}
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { AUTH_PAGE_ELEMENT } from '@/enums'
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { glassSurfaceFlat } from '@/lib/styles/glassSurface'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onChange: ((e: React.ChangeEvent<HTMLInputElement>) => void) &
|
onChange: ((e: React.ChangeEvent<HTMLInputElement>) => void) &
|
||||||
@@ -84,7 +85,7 @@ function StepNewPassword ({
|
|||||||
valid={!hasError()}
|
valid={!hasError()}
|
||||||
aria-errormessage={error}
|
aria-errormessage={error}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
className='mt-10 bg-container'
|
className={glassSurfaceFlat('mt-10')}
|
||||||
htmlFor={AUTH_PAGE_ELEMENT.INPUT_PASSWORD}
|
htmlFor={AUTH_PAGE_ELEMENT.INPUT_PASSWORD}
|
||||||
labelText={t('NewPassword.LabelPass')}
|
labelText={t('NewPassword.LabelPass')}
|
||||||
value={value}
|
value={value}
|
||||||
@@ -108,7 +109,7 @@ function StepNewPassword ({
|
|||||||
valid={!hasError(error2)}
|
valid={!hasError(error2)}
|
||||||
aria-errormessage={error2}
|
aria-errormessage={error2}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
className='mt-6 bg-container'
|
className={glassSurfaceFlat('mt-6')}
|
||||||
htmlFor={AUTH_PAGE_ELEMENT.INPUT_PASSWORD_REPEAT}
|
htmlFor={AUTH_PAGE_ELEMENT.INPUT_PASSWORD_REPEAT}
|
||||||
labelText={t('NewPassword.LabelRepeatPass')}
|
labelText={t('NewPassword.LabelRepeatPass')}
|
||||||
value={repeatValue}
|
value={repeatValue}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useBulkCart } from '@/app/[name]/(Main)/cart/hooks/useCartData'
|
|||||||
import { setRefreshToken, setToken } from '@/lib/api/func'
|
import { setRefreshToken, setToken } from '@/lib/api/func'
|
||||||
import { useSearchParams } from 'next/navigation'
|
import { useSearchParams } from 'next/navigation'
|
||||||
import { AUTH_PAGE_ELEMENT } from '@/enums'
|
import { AUTH_PAGE_ELEMENT } from '@/enums'
|
||||||
|
import { glassSurfaceFlat } from '@/lib/styles/glassSurface'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { formatCountdown } from '@/hooks/useCountdown'
|
import { formatCountdown } from '@/hooks/useCountdown'
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ const StepOtp = ({ phone, slug, timerRunning, secondsLeft, onResend, isResendPen
|
|||||||
type='tel'
|
type='tel'
|
||||||
autoComplete="one-time-code"
|
autoComplete="one-time-code"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
className='w-full h-[45px] sm:h-[50px] flex-1 mx-1 sm:mx-2 bg-container border border-border text-foreground outline-0 rounded-[10px] text-center text-sm' />
|
className={glassSurfaceFlat('w-full h-[45px] sm:h-[50px] flex-1 mx-1 sm:mx-2 border border-border text-foreground outline-0 rounded-[10px] text-center text-sm')} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,26 +2,40 @@
|
|||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
getRestaurantSlugFromPath,
|
||||||
|
restorePersistedRestaurantQueries,
|
||||||
|
} from "@/lib/helpers/themeCache";
|
||||||
|
|
||||||
|
function createQueryClient() {
|
||||||
|
const client = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: false,
|
||||||
|
retryOnMount: false,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const slug = getRestaurantSlugFromPath();
|
||||||
|
if (slug) {
|
||||||
|
restorePersistedRestaurantQueries(client, slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
export function ReactQueryProvider({
|
export function ReactQueryProvider({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [queryClient] = useState(
|
const [queryClient] = useState(createQueryClient);
|
||||||
() =>
|
|
||||||
new QueryClient({
|
|
||||||
defaultOptions: {
|
|
||||||
queries: {
|
|
||||||
retry: false,
|
|
||||||
retryOnMount: false,
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
retry: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
export const PATTERN_BACKGROUND_OPACITY = 0.05;
|
||||||
|
export const PATTERN_VECTOR_OPACITY = 0.1;
|
||||||
|
export const PRIMARY_LIGHT_OPACITY = 0.15;
|
||||||
|
|
||||||
|
export type BgType = "pattern" | "custom" | "color";
|
||||||
|
|
||||||
|
export type RestaurantBackgroundSettings = {
|
||||||
|
bgType?: BgType | string | null;
|
||||||
|
bgUrl?: string | null;
|
||||||
|
bgBlur?: number | string | null;
|
||||||
|
bgOpacity?: number | string | null;
|
||||||
|
bgOverlay?: string | null;
|
||||||
|
menuColor?: string | null;
|
||||||
|
isPattern?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeBgNumber = (value: unknown): number | null => {
|
||||||
|
if (value == null || value === "") return null;
|
||||||
|
const parsed = typeof value === "string" ? Number(value) : value;
|
||||||
|
if (typeof parsed !== "number" || Number.isNaN(parsed)) return null;
|
||||||
|
return parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hexToRgba = (hex: string, alpha: number): string => {
|
||||||
|
const normalized = hex.replace("#", "");
|
||||||
|
if (normalized.length !== 6) {
|
||||||
|
return `rgba(0, 0, 0, ${alpha})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = parseInt(normalized.slice(0, 2), 16);
|
||||||
|
const g = parseInt(normalized.slice(2, 4), 16);
|
||||||
|
const b = parseInt(normalized.slice(4, 6), 16);
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isUnsetBgValue = (value: unknown): boolean =>
|
||||||
|
value == null || value === "" || value === 0;
|
||||||
|
|
||||||
|
const isCustomImageBackgroundLegacy = (
|
||||||
|
settings: RestaurantBackgroundSettings,
|
||||||
|
): boolean => {
|
||||||
|
if (!settings.bgUrl) return false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
!isUnsetBgValue(settings.bgBlur) ||
|
||||||
|
!isUnsetBgValue(settings.bgOpacity) ||
|
||||||
|
!isUnsetBgValue(settings.bgOverlay)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveBgType = (
|
||||||
|
settings: RestaurantBackgroundSettings,
|
||||||
|
): BgType => {
|
||||||
|
if (settings.bgType === null) return "color";
|
||||||
|
|
||||||
|
const normalized = settings.bgType?.toLowerCase();
|
||||||
|
if (
|
||||||
|
normalized === "pattern" ||
|
||||||
|
normalized === "custom" ||
|
||||||
|
normalized === "color"
|
||||||
|
) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.isPattern === true) return "pattern";
|
||||||
|
if (settings.isPattern === false && settings.bgUrl && isCustomImageBackgroundLegacy(settings)) {
|
||||||
|
return "custom";
|
||||||
|
}
|
||||||
|
if (settings.isPattern === false) return "color";
|
||||||
|
if (isCustomImageBackgroundLegacy(settings)) return "custom";
|
||||||
|
if (settings.bgUrl) return "pattern";
|
||||||
|
|
||||||
|
return "color";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isCustomImageBackground = (
|
||||||
|
settings: RestaurantBackgroundSettings,
|
||||||
|
): boolean => resolveBgType(settings) === "custom";
|
||||||
|
|
||||||
|
export const isPatternBackground = (
|
||||||
|
settings: RestaurantBackgroundSettings,
|
||||||
|
): boolean => resolveBgType(settings) === "pattern";
|
||||||
|
|
||||||
|
export const isColorBackground = (
|
||||||
|
settings: RestaurantBackgroundSettings,
|
||||||
|
): boolean => resolveBgType(settings) === "color";
|
||||||
|
|
||||||
|
export const colorizeSvg = (
|
||||||
|
svg: string,
|
||||||
|
color: string,
|
||||||
|
vectorOpacity: number = PATTERN_VECTOR_OPACITY,
|
||||||
|
): string => {
|
||||||
|
let result = svg.replace(/fill="(?!black|none)[^"]+"/g, `fill="${color}"`);
|
||||||
|
result = result.replace(
|
||||||
|
/fill-opacity="[^"]+"/g,
|
||||||
|
`fill-opacity="${vectorOpacity}"`,
|
||||||
|
);
|
||||||
|
result = result.replace(
|
||||||
|
/(<(?:path|circle|rect|ellipse|polygon)[^>]*fill="(?!black|none)[^"]+")(?![^>]*fill-opacity)/g,
|
||||||
|
`$1 fill-opacity="${vectorOpacity}"`,
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyColorBackgroundCssVariables = (menuColor: string) => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.removeAttribute("data-pattern-bg");
|
||||||
|
root.removeAttribute("data-image-bg");
|
||||||
|
root.style.removeProperty("--bg-pattern-base");
|
||||||
|
root.style.removeProperty("--bg-pattern-vector");
|
||||||
|
root.style.setProperty(
|
||||||
|
"--background",
|
||||||
|
hexToRgba(menuColor, PATTERN_BACKGROUND_OPACITY),
|
||||||
|
);
|
||||||
|
root.style.setProperty(
|
||||||
|
"--primary-light",
|
||||||
|
hexToRgba(menuColor, PRIMARY_LIGHT_OPACITY),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyPatternCssVariables = (menuColor: string) => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
const baseTint = hexToRgba(menuColor, PATTERN_BACKGROUND_OPACITY);
|
||||||
|
|
||||||
|
root.setAttribute("data-pattern-bg", "true");
|
||||||
|
root.style.setProperty("--bg-pattern-base", baseTint);
|
||||||
|
root.style.setProperty(
|
||||||
|
"--bg-pattern-vector",
|
||||||
|
hexToRgba(menuColor, PATTERN_VECTOR_OPACITY),
|
||||||
|
);
|
||||||
|
root.style.setProperty(
|
||||||
|
"--primary-light",
|
||||||
|
hexToRgba(menuColor, PRIMARY_LIGHT_OPACITY),
|
||||||
|
);
|
||||||
|
root.style.setProperty("--background", baseTint);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearPatternCssVariables = () => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.removeAttribute("data-pattern-bg");
|
||||||
|
root.style.removeProperty("--bg-pattern-base");
|
||||||
|
root.style.removeProperty("--bg-pattern-vector");
|
||||||
|
root.style.removeProperty("--primary-light");
|
||||||
|
root.style.removeProperty("--background");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyCustomImageCssVariables = () => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.setAttribute("data-image-bg", "true");
|
||||||
|
root.removeAttribute("data-pattern-bg");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearCustomImageCssVariables = () => {
|
||||||
|
document.documentElement.removeAttribute("data-image-bg");
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function getThemeBootScript(): string {
|
||||||
|
return `(function(){try{function isNight(){try{var p=localStorage.getItem("preference-storage");if(!p)return false;var o=JSON.parse(p);return o&&o.state&&o.state.items&&o.state.items["night-mode"]&&o.state.items["night-mode"].value===true}catch(v){return false}}var night=isNight(),e=document.documentElement;e.setAttribute("data-theme",night?"dark":"light");var s=location.pathname.split("/").filter(Boolean)[0],t=null;if(s&&s!=="auth"){var r=localStorage.getItem("restaurant-theme:"+s);if(r)t=JSON.parse(r)}if(!t||t.version!==1||!t.menuColor){var l=localStorage.getItem("theme-primary-color");if(l&&!night){var x=l.replace("#","");if(x.length===6){var p=parseInt(x.slice(0,2),16),f=parseInt(x.slice(2,4),16),m=parseInt(x.slice(4,6),16),b="rgba("+p+","+f+","+m+",0.05)";e.style.setProperty("--background",b)}}return}function h(n,a){var u=n.replace("#","");if(u.length!==6)return null;var v=parseInt(u.slice(0,2),16),g=parseInt(u.slice(2,4),16),y=parseInt(u.slice(4,6),16);return"rgba("+v+","+g+","+y+","+a+")"}function isUnset(v){return v==null||v===""||v===0}function isCustomImg(th){if(!th.bgUrl)return false;return !isUnset(th.bgBlur)||!isUnset(th.bgOpacity)||!isUnset(th.bgOverlay)}function resolveBgType(th){if(th.bgType===null)return"color";var bt=(th.bgType||"").toLowerCase();if(bt==="pattern"||bt==="custom"||bt==="color")return bt;if(th.isPattern===true)return"pattern";if(th.isPattern===false&&th.bgUrl&&isCustomImg(th))return"custom";if(th.isPattern===false)return"color";if(isCustomImg(th))return"custom";if(th.bgUrl)return"pattern";return"color"}if(t.primaryOklch){e.style.setProperty("--primary",t.primaryOklch);e.style.setProperty("--primary-foreground",t.primaryForeground||"oklch(1 0 0)")}if(night)return;var bt=resolveBgType(t);if(bt==="color"){var k=t.backgroundTint||h(t.menuColor,0.05),z=t.primaryLight||h(t.menuColor,0.15);k&&e.style.setProperty("--background",k);z&&e.style.setProperty("--primary-light",z);return};if(bt==="pattern"){e.setAttribute("data-pattern-bg","true");var k=t.backgroundTint||h(t.menuColor,0.05),w=t.bgPatternVector||h(t.menuColor,0.1),z=t.primaryLight||h(t.menuColor,0.15);k&&(e.style.setProperty("--background",k),e.style.setProperty("--bg-pattern-base",k));w&&e.style.setProperty("--bg-pattern-vector",w);z&&e.style.setProperty("--primary-light",z);if(k){var d=document.createElement("div");d.id="cached-theme-bg";d.setAttribute("aria-hidden","true");d.style.cssText="position:fixed;inset:0;pointer-events:none;z-index:0;background-color:"+k+(t.patternDataUrl?';background-image:url("'+t.patternDataUrl+'");background-repeat:repeat;background-size:auto':"")+";";document.body.appendChild(d)}}else if(bt==="custom"){e.setAttribute("data-image-bg","true");var d2=document.createElement("div");d2.id="cached-theme-bg";d2.setAttribute("aria-hidden","true");d2.style.cssText="position:fixed;inset:0;pointer-events:none;z-index:0;overflow:hidden;";var i=document.createElement("div");i.id="cached-theme-bg-img";i.style.cssText="position:absolute;inset:-40px;background-size:cover;background-position:center;background-repeat:no-repeat;background-image:url(\\""+t.bgUrl+"\\");"+(t.bgBlur?"filter:blur("+t.bgBlur+"px);":"")+"opacity:"+(t.bgOpacity!=null?t.bgOpacity/100:1)+";";d2.appendChild(i);var ov=document.createElement("div");ov.id="cached-theme-bg-overlay";ov.style.cssText="position:absolute;inset:0;"+(t.bgOverlay&&Number(t.bgOverlay)>0?"background-color:rgba(0,0,0,"+Number(t.bgOverlay)/100+");":"");d2.appendChild(ov);document.body.appendChild(d2)}}catch(u){}})();`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,456 @@
|
|||||||
|
import { calculateBrightness, hexToOklch } from "@/lib/helpers/colorUtils";
|
||||||
|
import {
|
||||||
|
hexToRgba,
|
||||||
|
resolveBgType,
|
||||||
|
normalizeBgNumber,
|
||||||
|
PATTERN_BACKGROUND_OPACITY,
|
||||||
|
PATTERN_VECTOR_OPACITY,
|
||||||
|
PRIMARY_LIGHT_OPACITY,
|
||||||
|
type BgType,
|
||||||
|
type RestaurantBackgroundSettings,
|
||||||
|
} from "@/lib/helpers/backgroundUtils";
|
||||||
|
import type { AboutResponse } from "@/app/[name]/(Main)/about/types/Types";
|
||||||
|
import type {
|
||||||
|
CategoriesResponse,
|
||||||
|
FoodsResponse,
|
||||||
|
} from "@/app/[name]/(Main)/types/Types";
|
||||||
|
import type { QueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export type CachedRestaurantTheme = {
|
||||||
|
version: 1;
|
||||||
|
menuColor: string;
|
||||||
|
bgType?: BgType;
|
||||||
|
bgUrl?: string | null;
|
||||||
|
bgBlur?: number | null;
|
||||||
|
bgOpacity?: number | null;
|
||||||
|
bgOverlay?: string | null;
|
||||||
|
isPattern: boolean;
|
||||||
|
patternDataUrl?: string | null;
|
||||||
|
primaryOklch?: string;
|
||||||
|
primaryForeground?: string;
|
||||||
|
backgroundTint?: string;
|
||||||
|
bgPatternVector?: string;
|
||||||
|
primaryLight?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CACHE_VERSION = 1;
|
||||||
|
const CACHE_KEY_PREFIX = "restaurant-theme";
|
||||||
|
const LEGACY_COLOR_KEY = "theme-primary-color";
|
||||||
|
const ABOUT_CACHE_PREFIX = "restaurant-about";
|
||||||
|
const MENU_CACHE_PREFIX = "restaurant-menu";
|
||||||
|
const CATEGORIES_CACHE_PREFIX = "restaurant-categories";
|
||||||
|
export const THEME_BG_ELEMENT_ID = "cached-theme-bg";
|
||||||
|
const THEME_BG_IMG_ID = "cached-theme-bg-img";
|
||||||
|
const THEME_BG_OVERLAY_ID = "cached-theme-bg-overlay";
|
||||||
|
const PREFERENCE_STORAGE_KEY = "preference-storage";
|
||||||
|
|
||||||
|
export function isNightModeEnabled(): boolean {
|
||||||
|
if (typeof window === "undefined") return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(PREFERENCE_STORAGE_KEY);
|
||||||
|
if (!raw) return false;
|
||||||
|
const parsed = JSON.parse(raw) as {
|
||||||
|
state?: { items?: Record<string, { value?: unknown }> };
|
||||||
|
};
|
||||||
|
return parsed?.state?.items?.["night-mode"]?.value === true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearRestaurantBackgroundOverrides(): void {
|
||||||
|
if (typeof document === "undefined") return;
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.removeAttribute("data-pattern-bg");
|
||||||
|
root.removeAttribute("data-image-bg");
|
||||||
|
root.style.removeProperty("--background");
|
||||||
|
root.style.removeProperty("--bg-pattern-base");
|
||||||
|
root.style.removeProperty("--bg-pattern-vector");
|
||||||
|
root.style.removeProperty("--primary-light");
|
||||||
|
document.getElementById(THEME_BG_ELEMENT_ID)?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThemeCacheKey(slug: string): string {
|
||||||
|
return `${CACHE_KEY_PREFIX}:${slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAboutCacheKey(slug: string): string {
|
||||||
|
return `${ABOUT_CACHE_PREFIX}:${slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMenuCacheKey(slug: string): string {
|
||||||
|
return `${MENU_CACHE_PREFIX}:${slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCategoriesCacheKey(slug: string): string {
|
||||||
|
return `${CATEGORIES_CACHE_PREFIX}:${slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRestaurantSlugFromPath(pathname?: string): string | null {
|
||||||
|
const path =
|
||||||
|
pathname ?? (typeof window !== "undefined" ? window.location.pathname : "");
|
||||||
|
const segment = path.split("/").filter(Boolean)[0];
|
||||||
|
if (!segment || segment === "auth") return null;
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeThemeCssValues(menuColor: string) {
|
||||||
|
const primaryOklch = hexToOklch(menuColor);
|
||||||
|
const brightness = calculateBrightness(menuColor);
|
||||||
|
|
||||||
|
return {
|
||||||
|
primaryOklch: primaryOklch ?? undefined,
|
||||||
|
primaryForeground: brightness > 0.5 ? "oklch(0 0 0)" : "oklch(1 0 0)",
|
||||||
|
backgroundTint: hexToRgba(menuColor, PATTERN_BACKGROUND_OPACITY),
|
||||||
|
bgPatternVector: hexToRgba(menuColor, PATTERN_VECTOR_OPACITY),
|
||||||
|
primaryLight: hexToRgba(menuColor, PRIMARY_LIGHT_OPACITY),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThemeCache(slug: string): CachedRestaurantTheme | null {
|
||||||
|
if (!slug || typeof window === "undefined") return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(getThemeCacheKey(slug));
|
||||||
|
if (!raw) return null;
|
||||||
|
|
||||||
|
const parsed = JSON.parse(raw) as CachedRestaurantTheme;
|
||||||
|
if (parsed.version !== CACHE_VERSION || !parsed.menuColor) return null;
|
||||||
|
|
||||||
|
if (!parsed.primaryOklch) {
|
||||||
|
const enriched: CachedRestaurantTheme = {
|
||||||
|
...parsed,
|
||||||
|
...computeThemeCssValues(parsed.menuColor),
|
||||||
|
version: CACHE_VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(getThemeCacheKey(slug), JSON.stringify(enriched));
|
||||||
|
} catch {
|
||||||
|
// Ignore quota errors during migration
|
||||||
|
}
|
||||||
|
|
||||||
|
return enriched;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCachedMenuColor(slug?: string | null): string | null {
|
||||||
|
if (slug) {
|
||||||
|
const cached = getThemeCache(slug);
|
||||||
|
if (cached?.menuColor) return cached.menuColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window === "undefined") return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(LEGACY_COLOR_KEY);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPersistedAboutData(slug: string): AboutResponse | undefined {
|
||||||
|
if (!slug || typeof window === "undefined") return undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(getAboutCacheKey(slug));
|
||||||
|
if (!raw) return undefined;
|
||||||
|
return JSON.parse(raw) as AboutResponse;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setPersistedAboutData(slug: string, data: AboutResponse): void {
|
||||||
|
if (!slug || typeof window === "undefined") return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(getAboutCacheKey(slug), JSON.stringify(data));
|
||||||
|
} catch {
|
||||||
|
// Ignore quota errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPersistedQueryData<T>(key: string): T | undefined {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
if (!raw) return undefined;
|
||||||
|
return JSON.parse(raw) as T;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writePersistedQueryData<T>(key: string, data: T): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, JSON.stringify(data));
|
||||||
|
} catch {
|
||||||
|
// Ignore quota errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPersistedMenuData(slug: string): FoodsResponse | undefined {
|
||||||
|
if (!slug || typeof window === "undefined") return undefined;
|
||||||
|
return readPersistedQueryData<FoodsResponse>(getMenuCacheKey(slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setPersistedMenuData(slug: string, data: FoodsResponse): void {
|
||||||
|
if (!slug || typeof window === "undefined") return;
|
||||||
|
writePersistedQueryData(getMenuCacheKey(slug), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPersistedCategoriesData(
|
||||||
|
slug: string,
|
||||||
|
): CategoriesResponse | undefined {
|
||||||
|
if (!slug || typeof window === "undefined") return undefined;
|
||||||
|
return readPersistedQueryData<CategoriesResponse>(getCategoriesCacheKey(slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setPersistedCategoriesData(
|
||||||
|
slug: string,
|
||||||
|
data: CategoriesResponse,
|
||||||
|
): void {
|
||||||
|
if (!slug || typeof window === "undefined") return;
|
||||||
|
writePersistedQueryData(getCategoriesCacheKey(slug), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restorePersistedRestaurantQueries(
|
||||||
|
client: QueryClient,
|
||||||
|
slug: string,
|
||||||
|
): void {
|
||||||
|
const about = getPersistedAboutData(slug);
|
||||||
|
if (about) {
|
||||||
|
client.setQueryData(["about", slug], about);
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = getPersistedMenuData(slug);
|
||||||
|
if (menu) {
|
||||||
|
client.setQueryData(["menu", slug], menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories = getPersistedCategoriesData(slug);
|
||||||
|
if (categories) {
|
||||||
|
client.setQueryData(["categories", slug], categories);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setThemeCache(
|
||||||
|
slug: string,
|
||||||
|
theme: Omit<CachedRestaurantTheme, "version">,
|
||||||
|
): void {
|
||||||
|
if (!slug || typeof window === "undefined") return;
|
||||||
|
|
||||||
|
const existing = getThemeCache(slug);
|
||||||
|
const css = computeThemeCssValues(theme.menuColor);
|
||||||
|
const payload: CachedRestaurantTheme = {
|
||||||
|
...theme,
|
||||||
|
...css,
|
||||||
|
patternDataUrl:
|
||||||
|
theme.patternDataUrl !== undefined
|
||||||
|
? theme.patternDataUrl
|
||||||
|
: (existing?.patternDataUrl ?? null),
|
||||||
|
version: CACHE_VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(getThemeCacheKey(slug), JSON.stringify(payload));
|
||||||
|
localStorage.setItem(LEGACY_COLOR_KEY, theme.menuColor);
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
const { patternDataUrl: _pattern, ...withoutPattern } = payload;
|
||||||
|
localStorage.setItem(
|
||||||
|
getThemeCacheKey(slug),
|
||||||
|
JSON.stringify({ ...withoutPattern, patternDataUrl: null }),
|
||||||
|
);
|
||||||
|
localStorage.setItem(LEGACY_COLOR_KEY, theme.menuColor);
|
||||||
|
} catch {
|
||||||
|
// Ignore quota errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildThemeCacheFromRestaurant(
|
||||||
|
settings: RestaurantBackgroundSettings,
|
||||||
|
): Pick<
|
||||||
|
CachedRestaurantTheme,
|
||||||
|
| "menuColor"
|
||||||
|
| "bgType"
|
||||||
|
| "bgUrl"
|
||||||
|
| "bgBlur"
|
||||||
|
| "bgOpacity"
|
||||||
|
| "bgOverlay"
|
||||||
|
| "isPattern"
|
||||||
|
> {
|
||||||
|
const bgType = resolveBgType(settings);
|
||||||
|
|
||||||
|
return {
|
||||||
|
menuColor: settings.menuColor || "#1E3A8A",
|
||||||
|
bgType,
|
||||||
|
bgUrl: settings.bgUrl,
|
||||||
|
bgBlur: normalizeBgNumber(settings.bgBlur),
|
||||||
|
bgOpacity: normalizeBgNumber(settings.bgOpacity),
|
||||||
|
bgOverlay: settings.bgOverlay ?? null,
|
||||||
|
isPattern: bgType === "pattern",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function themeBackgroundMatches(
|
||||||
|
cached: CachedRestaurantTheme,
|
||||||
|
settings: RestaurantBackgroundSettings,
|
||||||
|
): boolean {
|
||||||
|
const next = buildThemeCacheFromRestaurant(settings);
|
||||||
|
return (
|
||||||
|
cached.menuColor === next.menuColor &&
|
||||||
|
cached.bgType === next.bgType &&
|
||||||
|
cached.bgUrl === next.bgUrl &&
|
||||||
|
cached.bgBlur === next.bgBlur &&
|
||||||
|
cached.bgOpacity === next.bgOpacity &&
|
||||||
|
cached.bgOverlay === next.bgOverlay &&
|
||||||
|
cached.isPattern === next.isPattern
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function svgToDataUrl(svg: string): string {
|
||||||
|
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureBgElement(): HTMLElement {
|
||||||
|
let bgElement = document.getElementById(THEME_BG_ELEMENT_ID);
|
||||||
|
if (!bgElement) {
|
||||||
|
bgElement = document.createElement("div");
|
||||||
|
bgElement.id = THEME_BG_ELEMENT_ID;
|
||||||
|
bgElement.setAttribute("aria-hidden", "true");
|
||||||
|
bgElement.style.position = "fixed";
|
||||||
|
bgElement.style.inset = "0";
|
||||||
|
bgElement.style.pointerEvents = "none";
|
||||||
|
bgElement.style.zIndex = "0";
|
||||||
|
document.body.appendChild(bgElement);
|
||||||
|
}
|
||||||
|
return bgElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyCachedThemeToDom(theme: CachedRestaurantTheme): void {
|
||||||
|
if (typeof document === "undefined") return;
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
if (theme.primaryOklch) {
|
||||||
|
root.style.setProperty("--primary", theme.primaryOklch);
|
||||||
|
root.style.setProperty(
|
||||||
|
"--primary-foreground",
|
||||||
|
theme.primaryForeground ?? "oklch(1 0 0)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNightModeEnabled()) {
|
||||||
|
clearRestaurantBackgroundOverrides();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bgType = resolveBgType(theme);
|
||||||
|
|
||||||
|
if (bgType === "color") {
|
||||||
|
document.getElementById(THEME_BG_ELEMENT_ID)?.remove();
|
||||||
|
const tint =
|
||||||
|
theme.backgroundTint ??
|
||||||
|
hexToRgba(theme.menuColor, PATTERN_BACKGROUND_OPACITY);
|
||||||
|
const primaryLight =
|
||||||
|
theme.primaryLight ??
|
||||||
|
hexToRgba(theme.menuColor, PRIMARY_LIGHT_OPACITY);
|
||||||
|
root.removeAttribute("data-pattern-bg");
|
||||||
|
root.removeAttribute("data-image-bg");
|
||||||
|
root.style.removeProperty("--bg-pattern-base");
|
||||||
|
root.style.removeProperty("--bg-pattern-vector");
|
||||||
|
root.style.setProperty("--background", tint);
|
||||||
|
root.style.setProperty("--primary-light", primaryLight);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bgType === "pattern") {
|
||||||
|
root.setAttribute("data-pattern-bg", "true");
|
||||||
|
root.removeAttribute("data-image-bg");
|
||||||
|
|
||||||
|
if (theme.backgroundTint) {
|
||||||
|
root.style.setProperty("--background", theme.backgroundTint);
|
||||||
|
root.style.setProperty("--bg-pattern-base", theme.backgroundTint);
|
||||||
|
}
|
||||||
|
if (theme.bgPatternVector) {
|
||||||
|
root.style.setProperty("--bg-pattern-vector", theme.bgPatternVector);
|
||||||
|
}
|
||||||
|
if (theme.primaryLight) {
|
||||||
|
root.style.setProperty("--primary-light", theme.primaryLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!theme.backgroundTint) return;
|
||||||
|
|
||||||
|
const bgElement = ensureBgElement();
|
||||||
|
bgElement.style.overflow = "";
|
||||||
|
|
||||||
|
document.getElementById(THEME_BG_IMG_ID)?.remove();
|
||||||
|
document.getElementById(THEME_BG_OVERLAY_ID)?.remove();
|
||||||
|
|
||||||
|
bgElement.style.backgroundColor = theme.backgroundTint;
|
||||||
|
if (theme.patternDataUrl) {
|
||||||
|
bgElement.style.backgroundImage = `url("${theme.patternDataUrl}")`;
|
||||||
|
bgElement.style.backgroundRepeat = "repeat";
|
||||||
|
bgElement.style.backgroundSize = "auto";
|
||||||
|
} else {
|
||||||
|
bgElement.style.removeProperty("background-image");
|
||||||
|
bgElement.style.removeProperty("background-repeat");
|
||||||
|
bgElement.style.removeProperty("background-size");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bgType === "custom" && theme.bgUrl) {
|
||||||
|
root.setAttribute("data-image-bg", "true");
|
||||||
|
root.removeAttribute("data-pattern-bg");
|
||||||
|
|
||||||
|
const bgElement = ensureBgElement();
|
||||||
|
bgElement.style.backgroundColor = "";
|
||||||
|
bgElement.style.backgroundImage = "";
|
||||||
|
bgElement.style.overflow = "hidden";
|
||||||
|
|
||||||
|
let imgLayer = document.getElementById(THEME_BG_IMG_ID);
|
||||||
|
if (!imgLayer) {
|
||||||
|
imgLayer = document.createElement("div");
|
||||||
|
imgLayer.id = THEME_BG_IMG_ID;
|
||||||
|
imgLayer.style.position = "absolute";
|
||||||
|
imgLayer.style.inset = "-40px";
|
||||||
|
imgLayer.style.backgroundSize = "cover";
|
||||||
|
imgLayer.style.backgroundPosition = "center";
|
||||||
|
imgLayer.style.backgroundRepeat = "no-repeat";
|
||||||
|
bgElement.appendChild(imgLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
imgLayer.style.backgroundImage = `url("${theme.bgUrl}")`;
|
||||||
|
imgLayer.style.filter = theme.bgBlur ? `blur(${theme.bgBlur}px)` : "";
|
||||||
|
imgLayer.style.opacity =
|
||||||
|
theme.bgOpacity != null ? String(theme.bgOpacity / 100) : "1";
|
||||||
|
|
||||||
|
let overlayLayer = document.getElementById(THEME_BG_OVERLAY_ID);
|
||||||
|
if (!overlayLayer) {
|
||||||
|
overlayLayer = document.createElement("div");
|
||||||
|
overlayLayer.id = THEME_BG_OVERLAY_ID;
|
||||||
|
overlayLayer.style.position = "absolute";
|
||||||
|
overlayLayer.style.inset = "0";
|
||||||
|
bgElement.appendChild(overlayLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlayOpacity =
|
||||||
|
theme.bgOverlay != null ? Number(theme.bgOverlay) / 100 : 0;
|
||||||
|
overlayLayer.style.backgroundColor =
|
||||||
|
overlayOpacity > 0 ? `rgba(0,0,0,${overlayOpacity})` : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeCachedThemeBackground(): void {
|
||||||
|
document.getElementById(THEME_BG_ELEMENT_ID)?.remove();
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import clsx, { type ClassValue } from "clsx";
|
||||||
|
|
||||||
|
export const GLASS_SURFACE = "glass-surface";
|
||||||
|
export const GLASS_SURFACE_FLAT = "glass-surface--flat";
|
||||||
|
export const GLASS_SURFACE_SELECTED = "glass-surface--selected";
|
||||||
|
export const GLASS_SURFACE_NAV = "glass-surface-nav";
|
||||||
|
|
||||||
|
export function glassSurfaceNav(...extra: ClassValue[]) {
|
||||||
|
return clsx(GLASS_SURFACE_NAV, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function glassSurface(...extra: ClassValue[]) {
|
||||||
|
return clsx(GLASS_SURFACE, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function glassSurfaceFlat(...extra: ClassValue[]) {
|
||||||
|
return clsx(GLASS_SURFACE, GLASS_SURFACE_FLAT, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function glassSurfaceSelected(...extra: ClassValue[]) {
|
||||||
|
return clsx(GLASS_SURFACE, GLASS_SURFACE_SELECTED, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Page-level card sections (about, cart, profile, etc.) */
|
||||||
|
export function glassSurfaceCard(...extra: ClassValue[]) {
|
||||||
|
return clsx(GLASS_SURFACE, "rounded-container", extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nested / secondary card sections with no shadow */
|
||||||
|
export function glassSurfaceCardFlat(...extra: ClassValue[]) {
|
||||||
|
return clsx(GLASS_SURFACE, GLASS_SURFACE_FLAT, "rounded-container", extra);
|
||||||
|
}
|
||||||
+3
-2
@@ -9,10 +9,11 @@ const HOST_MAP: Record<string, string> = {
|
|||||||
"cookieca.ir": "/cookieca",
|
"cookieca.ir": "/cookieca",
|
||||||
"felamingofood.ir": "/flamingo",
|
"felamingofood.ir": "/flamingo",
|
||||||
"nanakbakery.ir": "/nanak",
|
"nanakbakery.ir": "/nanak",
|
||||||
"shabcheragh.ir": "/shabcheragh",
|
"shabcheraghfood.ir": "/shabcheragh",
|
||||||
"fikastore.ir": "/fika",
|
"fikastore.ir": "/fika",
|
||||||
"easydizyarak.ir": "/easydeasy",
|
"easydizyarak.ir": "/easydizy",
|
||||||
"milcoplus.ir": "/milco",
|
"milcoplus.ir": "/milco",
|
||||||
|
"goodmoodchocolate.ir": "/goodmood",
|
||||||
// دامنههای جدید اینجا اضافه میشوند
|
// دامنههای جدید اینجا اضافه میشوند
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/* Glass surface — Figma: white 60%, border, shadow, backdrop blur */
|
||||||
|
|
||||||
|
.glass-surface {
|
||||||
|
background-color: rgba(255, 255, 255, 0.6);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
-webkit-backdrop-filter: blur(3px);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
box-shadow: 0px 2px 16px 0px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-pattern-bg="true"] .glass-surface,
|
||||||
|
html[data-image-bg="true"] .glass-surface {
|
||||||
|
-webkit-backdrop-filter: blur(1px);
|
||||||
|
backdrop-filter: blur(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .glass-surface {
|
||||||
|
background-color: color-mix(in oklch, var(--container) 60%, transparent);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-surface--flat {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-surface--selected {
|
||||||
|
background-color: rgba(255, 255, 255, 0.85) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .glass-surface--selected {
|
||||||
|
background-color: color-mix(in oklch, var(--container) 85%, transparent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-surface-nav {
|
||||||
|
background-color: rgba(255, 255, 255, 0.32);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
box-shadow: 0px -2px 14px 0px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-pattern-bg="true"] .glass-surface-nav,
|
||||||
|
html[data-image-bg="true"] .glass-surface-nav {
|
||||||
|
-webkit-backdrop-filter: blur(4px);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .glass-surface-nav {
|
||||||
|
background-color: color-mix(in oklch, var(--container) 32%, transparent);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user