Compare commits
2 Commits
b738d0c292
...
71238dcd75
| Author | SHA1 | Date | |
|---|---|---|---|
| 71238dcd75 | |||
| 5976e8ea44 |
@@ -3,7 +3,7 @@
|
|||||||
import { useCart } from "@/app/[name]/(Main)/cart/hook/useCart";
|
import { useCart } from "@/app/[name]/(Main)/cart/hook/useCart";
|
||||||
import MinusIcon from "@/components/icons/MinusIcon";
|
import MinusIcon from "@/components/icons/MinusIcon";
|
||||||
import PlusIcon from "@/components/icons/PlusIcon";
|
import PlusIcon from "@/components/icons/PlusIcon";
|
||||||
import { toast } 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 { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
@@ -52,7 +52,7 @@ function FoodPage({}: Props) {
|
|||||||
|
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
if (!token || !isSuccess) {
|
if (!token || !isSuccess) {
|
||||||
toast("ابتدا لاگین کنید", "error");
|
toastLoginRequired(() => router.push(`/${name}/auth`), "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+79
-23
@@ -2,11 +2,17 @@
|
|||||||
import { CloseCircle, InfoCircle, TickCircle } from 'iconsax-react';
|
import { CloseCircle, InfoCircle, TickCircle } from 'iconsax-react';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface ToastAction {
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface Toast {
|
interface Toast {
|
||||||
id: string;
|
id: string;
|
||||||
message: string;
|
message: string;
|
||||||
type?: 'success' | 'error' | 'info';
|
type?: 'success' | 'error' | 'info';
|
||||||
isExiting?: boolean;
|
isExiting?: boolean;
|
||||||
|
action?: ToastAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
let addToast: (toast: Toast) => void;
|
let addToast: (toast: Toast) => void;
|
||||||
@@ -18,7 +24,8 @@ const ToastContainer: React.FC = () => {
|
|||||||
const showToast = (toast: Toast) => {
|
const showToast = (toast: Toast) => {
|
||||||
setToasts((prev) => [...prev, toast]);
|
setToasts((prev) => [...prev, toast]);
|
||||||
|
|
||||||
// حذف خودکار بعد از ۳ ثانیه
|
const dismissAfter = toast.action ? 6000 : 3000;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setToasts((prev) => prev.map(t =>
|
setToasts((prev) => prev.map(t =>
|
||||||
t.id === toast.id ? { ...t, isExiting: true } : t
|
t.id === toast.id ? { ...t, isExiting: true } : t
|
||||||
@@ -28,7 +35,16 @@ const ToastContainer: React.FC = () => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setToasts((prev) => prev.filter((t) => t.id !== toast.id));
|
setToasts((prev) => prev.filter((t) => t.id !== toast.id));
|
||||||
}, 300); // مدت زمان انیمیشن خروج
|
}, 300); // مدت زمان انیمیشن خروج
|
||||||
}, 3000);
|
}, dismissAfter);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dismissToast = (id: string) => {
|
||||||
|
setToasts((prev) => prev.map((t) =>
|
||||||
|
t.id === id ? { ...t, isExiting: true } : t
|
||||||
|
));
|
||||||
|
setTimeout(() => {
|
||||||
|
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||||
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
// تخصیص تابع نمایش toast به متغیر سراسری
|
// تخصیص تابع نمایش toast به متغیر سراسری
|
||||||
@@ -38,36 +54,76 @@ const ToastContainer: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed top-4 text-sm right-0 left-0 mx-auto w-fit z-50 flex flex-col gap-2">
|
<div className="fixed top-4 text-sm right-0 left-0 mx-auto w-fit z-50 flex flex-col gap-2">
|
||||||
{toasts.map((toast) => (
|
{toasts.map((toast) => {
|
||||||
<div
|
const handleAction = () => {
|
||||||
key={toast.id}
|
toast.action?.onClick();
|
||||||
className={`px-4 flex items-center gap-2 backdrop-blur-2xl h-16 min-w-[300px] rounded-2xl shadow-md
|
dismissToast(toast.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={toast.id}
|
||||||
|
role={toast.action ? 'button' : undefined}
|
||||||
|
tabIndex={toast.action ? 0 : undefined}
|
||||||
|
onClick={toast.action ? handleAction : undefined}
|
||||||
|
onKeyDown={toast.action ? (e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleAction();
|
||||||
|
}
|
||||||
|
} : undefined}
|
||||||
|
className={`px-4 flex items-center gap-2 backdrop-blur-2xl h-16 min-w-[300px] rounded-2xl shadow-md
|
||||||
${toast.isExiting ? 'animate-slide-out-right' : 'animate-slide-in-right'} ${toast.type === 'success'
|
${toast.isExiting ? 'animate-slide-out-right' : 'animate-slide-in-right'} ${toast.type === 'success'
|
||||||
? 'bg-white/70 text-black'
|
? 'bg-white/70 text-black'
|
||||||
: toast.type === 'error'
|
: toast.type === 'error'
|
||||||
? 'bg-white/70 text-black'
|
? 'bg-white/70 text-black'
|
||||||
: 'bg-white/70 text-black'
|
: 'bg-white/70 text-black'
|
||||||
}`}
|
} ${toast.action ? 'cursor-pointer active:scale-[0.98] transition-transform' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
animationFillMode: 'forwards',
|
animationFillMode: 'forwards',
|
||||||
animationDuration: '0.3s',
|
animationDuration: '0.3s',
|
||||||
animationTimingFunction: 'ease-out'
|
animationTimingFunction: 'ease-out'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
toast.type === 'success' ? <TickCircle className='size-5' color='green' /> :
|
{
|
||||||
toast.type === 'error' ? <CloseCircle className='size-5' color='red' /> :
|
toast.type === 'success' ? <TickCircle className='size-5 shrink-0' color='green' /> :
|
||||||
<InfoCircle className='size-5' color='blue' />
|
toast.type === 'error' ? <CloseCircle className='size-5 shrink-0' color='red' /> :
|
||||||
}
|
<InfoCircle className='size-5 shrink-0' color='blue' />
|
||||||
{toast.message}
|
}
|
||||||
</div>
|
<span className="truncate">{toast.message}</span>
|
||||||
))}
|
</div>
|
||||||
|
{toast.action && (
|
||||||
|
<span className="shrink-0 rounded-xl bg-primary px-3 py-1.5 text-xs font-medium text-white pointer-events-none">
|
||||||
|
{toast.action.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toast = (message?: string, type: 'success' | 'error' | 'info' = 'info') => {
|
type ToastOptions = {
|
||||||
addToast({ id: Date.now().toString(), message: message || '', type });
|
action?: ToastAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toast = (
|
||||||
|
message?: string,
|
||||||
|
type: 'success' | 'error' | 'info' = 'info',
|
||||||
|
options?: ToastOptions
|
||||||
|
) => {
|
||||||
|
addToast({ id: Date.now().toString(), message: message || '', type, action: options?.action });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toastLoginRequired = (
|
||||||
|
onLogin: () => void,
|
||||||
|
type: 'success' | 'error' | 'info' = 'info'
|
||||||
|
) => {
|
||||||
|
toast('ابتدا لاگین کنید', type, {
|
||||||
|
action: { label: 'ورود', onClick: onLogin },
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ToastContainer;
|
export default ToastContainer;
|
||||||
@@ -18,7 +18,7 @@ import useToggle from '@/hooks/helpers/useToggle';
|
|||||||
import { useGetAbout } from '@/app/[name]/(Main)/about/hooks/useAboutData';
|
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 } from '../Toast';
|
import { toast, toastLoginRequired } from '../Toast';
|
||||||
|
|
||||||
type MenuItemType = {
|
type MenuItemType = {
|
||||||
href: string | undefined;
|
href: string | undefined;
|
||||||
@@ -203,7 +203,7 @@ function SideMenu({ menuState, toggleMenuState, nightModeState, togglenightModeS
|
|||||||
const handleNotificationClick = (e: React.MouseEvent) => {
|
const handleNotificationClick = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
toast('ابتدا لاگین کنید', 'info');
|
toastLoginRequired(() => router.push(`/${name}/auth`), 'info');
|
||||||
} else {
|
} else {
|
||||||
router.push(`/${name}/notifications`);
|
router.push(`/${name}/notifications`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ 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 { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData';
|
||||||
import { toast } from '../Toast';
|
import { toastLoginRequired } from '../Toast';
|
||||||
|
|
||||||
type BottomNavBarProps = {
|
type BottomNavBarProps = {
|
||||||
onPagerClick?: () => void;
|
onPagerClick?: () => void;
|
||||||
@@ -49,8 +49,7 @@ function BottomNavBar({ onPagerClick }: BottomNavBarProps) {
|
|||||||
router.push(`/${name}/favorite`);
|
router.push(`/${name}/favorite`);
|
||||||
} else {
|
} else {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toast('ابتدا لاگین کنید', 'error');
|
toastLoginRequired(() => router.push(`/${name}/auth`), 'error');
|
||||||
router.replace(`/${name}/auth`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import useToggle from '@/hooks/helpers/useToggle'
|
|||||||
import LogoutPrompt from '@/features/general/LogoutPrompt'
|
import LogoutPrompt from '@/features/general/LogoutPrompt'
|
||||||
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData'
|
import { useGetProfile } from '@/app/[name]/(Profile)/profile/hooks/userProfileData'
|
||||||
import { useGetNotificationsCount } from '@/app/[name]/(Main)/hooks/useMenuData'
|
import { useGetNotificationsCount } from '@/app/[name]/(Main)/hooks/useMenuData'
|
||||||
import { toast } from '../Toast'
|
import { toastLoginRequired } from '../Toast'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
profileDropState: boolean,
|
profileDropState: boolean,
|
||||||
@@ -40,7 +40,7 @@ function TopBar({ profileDropState, toggleProfileDropState, toggleMenuState }: P
|
|||||||
const handleNotificationClick = (e: React.MouseEvent) => {
|
const handleNotificationClick = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
toast('ابتدا لاگین کنید', 'info');
|
toastLoginRequired(() => router.push(`/${name}/auth`), 'info');
|
||||||
} else {
|
} else {
|
||||||
router.push(`/${name}/notifications`);
|
router.push(`/${name}/notifications`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
AUTH_PAGE_ELEMENT as AUTH_PAGE_ELEMENTS
|
AUTH_PAGE_ELEMENT as AUTH_PAGE_ELEMENTS
|
||||||
} from '@/enums'
|
} from '@/enums'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { formatCountdown } from '@/hooks/useCountdown'
|
||||||
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'
|
||||||
@@ -22,15 +23,6 @@ type Props = {
|
|||||||
pending?: boolean | undefined
|
pending?: boolean | undefined
|
||||||
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'id'>
|
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'id'>
|
||||||
|
|
||||||
const formatCountdown = (seconds: number) => {
|
|
||||||
const mins = Math.floor(seconds / 60)
|
|
||||||
const secs = seconds % 60
|
|
||||||
if (mins > 0) {
|
|
||||||
return `${mins}:${String(secs).padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
return String(seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
function StepEnterOtp ({
|
function StepEnterOtp ({
|
||||||
onChange,
|
onChange,
|
||||||
onClick,
|
onClick,
|
||||||
@@ -96,7 +88,7 @@ function StepEnterOtp ({
|
|||||||
<div>
|
<div>
|
||||||
{timerRunning ? (
|
{timerRunning ? (
|
||||||
<div>
|
<div>
|
||||||
{t('OTP.TimerRunning', { time: formatCountdown(secondsLeft) })}
|
{t('OTP.TimerRunning', { time: formatCountdown(secondsLeft, t) })}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ 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 clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { formatCountdown } from '@/hooks/useCountdown'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
phone: string
|
phone: string
|
||||||
@@ -24,15 +25,6 @@ type Props = {
|
|||||||
isResendPending?: boolean
|
isResendPending?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatCountdown = (seconds: number) => {
|
|
||||||
const mins = Math.floor(seconds / 60)
|
|
||||||
const secs = seconds % 60
|
|
||||||
if (mins > 0) {
|
|
||||||
return `${mins}:${String(secs).padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
return String(seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
const StepOtp = ({ phone, slug, timerRunning, secondsLeft, onResend, isResendPending }: Props) => {
|
const StepOtp = ({ phone, slug, timerRunning, secondsLeft, onResend, isResendPending }: Props) => {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const redirectUrl = searchParams.get('redirect')
|
const redirectUrl = searchParams.get('redirect')
|
||||||
@@ -126,7 +118,7 @@ const StepOtp = ({ phone, slug, timerRunning, secondsLeft, onResend, isResendPen
|
|||||||
<div className='text-center w-full pt-6 text-xs'>
|
<div className='text-center w-full pt-6 text-xs'>
|
||||||
{timerRunning ? (
|
{timerRunning ? (
|
||||||
<div>
|
<div>
|
||||||
{t('OTP.TimerRunning', { time: formatCountdown(secondsLeft) })}
|
{t('OTP.TimerRunning', { time: formatCountdown(secondsLeft, t) })}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
|
import { ef } from '@/lib/helpers/utfNumbers';
|
||||||
|
import type { TFunction } from 'i18next';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
export const formatCountdown = (seconds: number, t: TFunction) => {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
|
||||||
|
if (mins > 0 && secs > 0) {
|
||||||
|
return t('OTP.Countdown.MinuteAndSecond', { mins: ef(mins), secs: ef(secs) });
|
||||||
|
}
|
||||||
|
if (mins > 0) {
|
||||||
|
return t('OTP.Countdown.Minute', { count: ef(mins) });
|
||||||
|
}
|
||||||
|
return t('OTP.Countdown.Second', { count: ef(secs) });
|
||||||
|
};
|
||||||
|
|
||||||
export const useCountdown = (shouldStart: boolean, duration: number = 30, delay: number = 1000) => {
|
export const useCountdown = (shouldStart: boolean, duration: number = 30, delay: number = 1000) => {
|
||||||
const [timerRunning, setTimerRunning] = useState(false);
|
const [timerRunning, setTimerRunning] = useState(false);
|
||||||
const [secondsLeft, setSecondsLeft] = useState(duration);
|
const [secondsLeft, setSecondsLeft] = useState(duration);
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
"Description": "Please enter the 6-digit code sent to the number {{phoneNumber}}.",
|
"Description": "Please enter the 6-digit code sent to the number {{phoneNumber}}.",
|
||||||
"Label": "",
|
"Label": "",
|
||||||
"TimerRunning": "Resend available in {{time}}",
|
"TimerRunning": "Resend available in {{time}}",
|
||||||
|
"Countdown": {
|
||||||
|
"Minute": "{{count}} minute",
|
||||||
|
"Second": "{{count}} second",
|
||||||
|
"MinuteAndSecond": "{{mins}} minute and {{secs}} second"
|
||||||
|
},
|
||||||
"TimerOut": "Didn’t receive the code?",
|
"TimerOut": "Didn’t receive the code?",
|
||||||
"TimerReset": "Resend",
|
"TimerReset": "Resend",
|
||||||
"ButtonSubmit": "Next"
|
"ButtonSubmit": "Next"
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
"Description": "کد ۵ رقمی ارسال شده به شماره {{phoneNumber}} را وارد نمایید.",
|
"Description": "کد ۵ رقمی ارسال شده به شماره {{phoneNumber}} را وارد نمایید.",
|
||||||
"Label": "",
|
"Label": "",
|
||||||
"TimerRunning": "ارسال مجدد تا {{time}} دیگر",
|
"TimerRunning": "ارسال مجدد تا {{time}} دیگر",
|
||||||
|
"Countdown": {
|
||||||
|
"Minute": "{{count}} دقیقه",
|
||||||
|
"Second": "{{count}} ثانیه",
|
||||||
|
"MinuteAndSecond": "{{mins}} دقیقه و {{secs}} ثانیه"
|
||||||
|
},
|
||||||
"TimerOut": "کد را دریافت نکردید؟",
|
"TimerOut": "کد را دریافت نکردید؟",
|
||||||
"TimerReset": "ارسال مجدد",
|
"TimerReset": "ارسال مجدد",
|
||||||
"ButtonSubmit": "بعدی"
|
"ButtonSubmit": "بعدی"
|
||||||
|
|||||||
Reference in New Issue
Block a user