otp request + otp verify + component toast

This commit is contained in:
hamid zarghami
2025-11-11 12:35:44 +03:30
parent 04f3b3b9b5
commit bd2f5b33c1
16 changed files with 488 additions and 654 deletions
+73
View File
@@ -0,0 +1,73 @@
'use client'
import { CloseCircle, InfoCircle, TickCircle } from 'iconsax-react';
import React, { useState, useEffect } from 'react';
interface Toast {
id: string;
message: string;
type?: 'success' | 'error' | 'info';
isExiting?: boolean;
}
let addToast: (toast: Toast) => void;
const ToastContainer: React.FC = () => {
const [toasts, setToasts] = useState<Toast[]>([]);
// ثبت toast جدید
const showToast = (toast: Toast) => {
setToasts((prev) => [...prev, toast]);
// حذف خودکار بعد از ۳ ثانیه
setTimeout(() => {
setToasts((prev) => prev.map(t =>
t.id === toast.id ? { ...t, isExiting: true } : t
));
// حذف از DOM بعد از اتمام انیمیشن
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== toast.id));
}, 300); // مدت زمان انیمیشن خروج
}, 3000);
};
// تخصیص تابع نمایش toast به متغیر سراسری
useEffect(() => {
addToast = showToast;
}, []);
return (
<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) => (
<div
key={toast.id}
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'
? 'bg-white/70 text-black'
: toast.type === 'error'
? 'bg-white/70 text-black'
: 'bg-white/70 text-black'
}`}
style={{
animationFillMode: 'forwards',
animationDuration: '0.3s',
animationTimingFunction: 'ease-out'
}}
>
{
toast.type === 'success' ? <TickCircle className='size-5' color='green' /> :
toast.type === 'error' ? <CloseCircle className='size-5' color='red' /> :
<InfoCircle className='size-5' color='blue' />
}
{toast.message}
</div>
))}
</div>
);
};
export const toast = (message?: string, type: 'success' | 'error' | 'info' = 'info') => {
addToast({ id: Date.now().toString(), message: message || '', type });
};
export default ToastContainer;
+29 -24
View File
@@ -12,30 +12,35 @@ type Props = {
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "id">;
function InputField({ onChange, htmlFor, labelText, children, valid = true, className, ...inputProps }: Props) {
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 bg-inherit text-[12px] text-foreground'
htmlFor={htmlFor}>
{labelText}
</label>
const InputField = React.forwardRef<HTMLInputElement, Props>(
({ onChange, htmlFor, labelText, children, valid = true, className, ...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 bg-inherit text-[12px] text-foreground'
htmlFor={htmlFor}>
{labelText}
</label>
<Tooltip content={inputProps['aria-errormessage']} hidden={!inputProps['aria-errormessage']}>
<input
onChange={onChange}
className={clsx(
inputProps['aria-errormessage'] && '!text-red-300',
'py-2.5 pt-3.5 text-sm2 w-full outline-0 !leading-6'
)}
name={htmlFor}
{...inputProps} />
{children}
</Tooltip>
</div>
)
}
<Tooltip content={inputProps['aria-errormessage']} hidden={!inputProps['aria-errormessage']}>
<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'
)}
name={htmlFor}
{...inputProps} />
{children}
</Tooltip>
</div>
)
}
)
InputField.displayName = 'InputField'
export default InputField