chore: seperated auth page files

This commit is contained in:
Mahyar Khanbolooki
2025-06-30 23:02:12 +03:30
parent 5b58b1e6ba
commit 2fc5b28925
9 changed files with 289 additions and 172 deletions
+60 -156
View File
@@ -1,22 +1,14 @@
'use client';
import React, { useState, type FormEvent, type ChangeEvent, useEffect, useRef } from 'react'
import PrimaryButton from '@/components/button/PrimaryButton'
import { EyeToggleIcon } from '@/components/icons/EyeToggleIcon';
import InputField from '@/components/input/InputField';
import OTPInputField from '@/components/input/MultiInputField';
enum LOGIN_STEPS {
ENTER_NUMBER = 0,
ENTER_PASSWORD = 1,
ENTER_OTP = 2,
}
enum CLICK_ID {
TOGGLE_PASSWORD = "togglePassword",
RESET_PASSWORD = "resetPassword",
RESEND_OTP = "resendOtp"
}
import React, { useState, type FormEvent, type ChangeEvent, useEffect } from 'react'
import { useAuthStore } from '@/zustand/userStore';
import { loginUser } from '@/features/auth/actions/loginUser';
import { redirect } from 'next/navigation';
import StepEnterPassword from '@/features/auth/components/StepEnterPassword';
import StepEnterNumber from '@/features/auth/components/StepEnterNumber';
import StepEnterOtp from '@/features/auth/components/StepEnterOtp';
import { AUTH_PAGE_ELEMENT, AUTH_STEP } from '@/enums';
import { useCountdown } from '@/hooks/useCountdown';
type Props = object
@@ -26,45 +18,16 @@ function AuthIndex({ }: Props) {
const [otp, setOtp] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const [step, setStep] = useState(LOGIN_STEPS.ENTER_NUMBER);
const [timerRunning, setTimerRunning] = useState(false);
const [secondLeft, setSecondsLeft] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null); // ← useRef for interval
const [step, setStep] = useState(AUTH_STEP.ENTER_NUMBER);
const { timerRunning, secondsLeft, restart } = useCountdown(step === AUTH_STEP.ENTER_OTP);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated)
useEffect(() => {
if (timerRunning && intervalRef.current === null) {
intervalRef.current = setInterval(() => {
setSecondsLeft((state) => {
if (state <= 1) { // changed to <= 1 to prevent negative values
setTimerRunning(false);
clearInterval(intervalRef.current!);
intervalRef.current = null;
return 0;
}
return state - 1;
});
}, 1000);
if (isAuthenticated) {
redirect("/");
}
return () => {
// Cleanup if component unmounts or timerRunning changes
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [timerRunning]);
useEffect(() => {
if (step == LOGIN_STEPS.ENTER_OTP) {
setSecondsLeft(30);
setTimerRunning(true);
}
return () => {
}
}, [step])
}, [isAuthenticated])
const updateInput = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.id == "phone") {
@@ -89,133 +52,74 @@ function AuthIndex({ }: Props) {
e.stopPropagation();
const target = e.target as HTMLButtonElement;
if (target.id === CLICK_ID.TOGGLE_PASSWORD) {
if (target.id === AUTH_PAGE_ELEMENT.TOGGLE_PASSWORD) {
setShowPassword((state) => !state);
}
else if (target.id === CLICK_ID.RESET_PASSWORD) {
else if (target.id === AUTH_PAGE_ELEMENT.RESET_PASSWORD) {
setShowPassword((state) => !state);
}
else if (target.id === CLICK_ID.RESEND_OTP) {
setSecondsLeft(30);
setTimerRunning(true);
else if (target.id === AUTH_PAGE_ELEMENT.RESEND_OTP) {
if(!timerRunning) {
restart();
}
}
};
const submit = (e: FormEvent<HTMLFormElement>) => {
const submit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("STEP: ", step)
if (step == LOGIN_STEPS.ENTER_NUMBER) {
setStep(LOGIN_STEPS.ENTER_PASSWORD)
if (step == AUTH_STEP.ENTER_NUMBER) {
setStep(AUTH_STEP.ENTER_PASSWORD)
}
if (step == LOGIN_STEPS.ENTER_PASSWORD) {
setStep(LOGIN_STEPS.ENTER_OTP)
if (step == AUTH_STEP.ENTER_PASSWORD) {
if (await loginUser(number, password)) {
console.log("Logged in")
}
else {
console.log("Wrong credentials")
}
}
if (step == LOGIN_STEPS.ENTER_OTP) {
setStep(LOGIN_STEPS.ENTER_NUMBER)
setTimerRunning(false);
if (step == AUTH_STEP.ENTER_OTP) {
setStep(AUTH_STEP.ENTER_NUMBER)
}
}
if (step == LOGIN_STEPS.ENTER_NUMBER) {
if (step == AUTH_STEP.ENTER_NUMBER) {
// First Step -> Enter number
return (
<form onSubmit={submit} className='p-6 h-full flex flex-col justify-between'>
<div className='block'>
<img src='/assets/images/login-banner.png' />
<div className='pt-4' dir='rtl'>
<h6 className='text-lg font-bold'>ورود به سیستم</h6>
<p className='mt-3 text-[13px]'>برای ورود شماره همراه خود را وارد نمایید.</p>
</div>
<InputField
className='mt-10'
htmlFor='phone'
labelText='شماره همراه'
value={number}
placeholder='09120000000'
onChange={updateInput}
type='number' />
</div>
<PrimaryButton type='submit'>بعدی</PrimaryButton>
</form>
<StepEnterNumber
onChange={updateInput}
onSubmit={submit}
value={number}
/>
)
}
if (step == LOGIN_STEPS.ENTER_PASSWORD) {
if (step == AUTH_STEP.ENTER_PASSWORD) {
return (
<form onSubmit={submit} className='p-6 h-full flex flex-col justify-between'>
<div className='block'>
<img src='/assets/images/login-banner.png' />
<div className='pt-4' dir='rtl'>
<h6 className='text-lg font-bold'>ورود به سیستم</h6>
<p className='mt-3 text-[13px]'>کلمه عبور خود را وارد نمایید.</p>
</div>
<InputField
className='mt-10'
htmlFor='password'
labelText='کلمه عبور'
value={password}
placeholder=''
onChange={updateInput}
type={showPassword ? 'text' : 'password'}>
<button className='ps-3' id={CLICK_ID.TOGGLE_PASSWORD} type='button' onClick={onClickEvent}>
<EyeToggleIcon slash={showPassword} className="pointer-events-none" />
</button>
</InputField>
<div className='inline-flex justify-between items-center w-full pt-3'>
<button id={CLICK_ID.RESET_PASSWORD} type='button' onClick={onClickEvent} className='text-sm2 cursor-pointer'>بازیابی کلمه عبور</button>
<div className='inline-flex justify-between items-center'>
{/* TODO: customize checkbox */}
<label htmlFor='rememberMe' className='text-sm2 px-2 py-0.5'>مرا به خاطر بسپار</label>
<input id='rememberMe' className='h-4.5 w-4.5 checked:accent-primary' onChange={updateInput} type='checkbox' checked={rememberMe} /> </div>
</div>
</div>
<PrimaryButton type='submit'>ورود به منو</PrimaryButton>
</form>
<StepEnterPassword
onChange={updateInput}
onClick={onClickEvent}
onSubmit={submit}
value={password}
passwordVisible={showPassword}
rememberMe={rememberMe}
/>
)
}
if (step == LOGIN_STEPS.ENTER_OTP) {
if (step == AUTH_STEP.ENTER_OTP) {
return (
<form onSubmit={submit} className='p-6 h-full flex flex-col justify-between'>
<div className='block'>
<img src='/assets/images/login-banner.png' />
<div className='pt-4' dir='rtl'>
{/* // TODO: add persian digits font */}
<h6 className='text-lg font-bold'>کد فعالسازی را وارد کنید</h6>
<p className='mt-3 text-[13px]'>کد ۶ رقمی ارسال شده به شماره {number} را وارد نمایید.</p>
</div>
<OTPInputField
className='mt-10'
maxLength={6}
htmlFor='otp'
labelText=''
value={otp}
placeholder=''
onChange={updateInput}
type='text'>
</OTPInputField>
<div className='inline-flex justify-center items-center w-full pt-6 text-xs'>
<div dir='rtl'>
{timerRunning ?
<div>{secondLeft} ثانیه دیگر تا دریافت کد</div> :
<div>
کد را دریافت نکردید؟
<span className='text-primary px-1'>
<button id={CLICK_ID.RESEND_OTP} className='cursor-pointer' onClick={onClickEvent}>
دوباره امتحان کنید
</button>
</span>
</div>
}
</div>
</div>
</div>
<PrimaryButton type='submit'>بعدی</PrimaryButton>
</form>
<StepEnterOtp
onChange={updateInput}
onClick={onClickEvent}
phoneNumber={number}
onSubmit={submit}
value={otp}
timerRunning={timerRunning}
secondsLeft={secondsLeft}
/>
)
}
}
export default AuthIndex
export default AuthIndex
+5
View File
@@ -0,0 +1,5 @@
export enum AUTH_STEP {
ENTER_NUMBER = 0,
ENTER_PASSWORD = 1,
ENTER_OTP = 2,
}
+5
View File
@@ -0,0 +1,5 @@
export enum AUTH_PAGE_ELEMENT {
TOGGLE_PASSWORD = "togglePassword",
RESET_PASSWORD = "resetPassword",
RESEND_OTP = "resendOtp"
}
+5
View File
@@ -0,0 +1,5 @@
import { AUTH_STEP } from "./authStep";
import { AUTH_PAGE_ELEMENT } from "./elementIds";
export { AUTH_STEP };
export { AUTH_PAGE_ELEMENT };
+19 -16
View File
@@ -3,22 +3,25 @@ import { useAuthStore } from '@/zustand/userStore'
// Simulated API login
export async function loginUser(number: string, password: string) {
// Simulate API call - replace with real one
const response = await new Promise<{ user: any }>((resolve) =>
setTimeout(() => {
resolve({
user: {
id: '123',
name: 'John Doe',
number,
token: 'mock-jwt-token',
},
})
}, 1000)
)
if (number == "123" && password == "123") {
// Simulate API call - replace with real one
const response = await new Promise<{ user: any }>((resolve) =>
setTimeout(() => {
resolve({
user: {
id: '123',
name: 'John Doe',
number,
token: 'mock-jwt-token',
},
})
}, 1000)
)
const { login } = useAuthStore.getState()
login(response.user)
const { login } = useAuthStore.getState()
login(response.user)
return true;
}
return true;
return false;
}
@@ -0,0 +1,34 @@
import PrimaryButton from '@/components/button/PrimaryButton';
import InputField from '@/components/input/InputField';
import React from 'react'
type Props = {
onSubmit: React.FormEventHandler<HTMLFormElement> | undefined;
onChange: ((e: React.ChangeEvent<HTMLInputElement>) => void) & React.ChangeEventHandler<HTMLInputElement>;
value: string;
}
function StepEnterNumber({ onSubmit, onChange, value }: Props) {
return (
<form onSubmit={onSubmit} className='p-6 h-full flex flex-col justify-between'>
<div className='block'>
<img src='/assets/images/login-banner.png' />
<div className='pt-4' dir='rtl'>
<h6 className='text-lg font-bold'>ورود به سیستم</h6>
<p className='mt-3 text-[13px]'>برای ورود شماره همراه خود را وارد نمایید.</p>
</div>
<InputField
className='mt-10'
htmlFor='phone'
labelText='شماره همراه'
value={value}
placeholder='09120000000'
onChange={onChange}
type='number' />
</div>
<PrimaryButton type='submit'>بعدی</PrimaryButton>
</form>
)
}
export default StepEnterNumber
@@ -0,0 +1,59 @@
import PrimaryButton from '@/components/button/PrimaryButton';
import OTPInputField from '@/components/input/MultiInputField';
import { AUTH_PAGE_ELEMENT as AUTH_PAGE_ELEMENTS } from '@/enums';
import React from 'react'
type Props = {
onSubmit: React.FormEventHandler<HTMLFormElement> | undefined;
onChange: ((e: React.ChangeEvent<HTMLInputElement>) => void) & React.ChangeEventHandler<HTMLInputElement>;
onClick: React.MouseEventHandler<HTMLButtonElement> | undefined;
value: string;
phoneNumber: string;
timerRunning: boolean;
secondsLeft: number;
}
function StepEnterOtp({ onSubmit, onChange, onClick, value, phoneNumber, timerRunning, secondsLeft }: Props) {
return (
<form onSubmit={onSubmit} className='p-6 h-full flex flex-col justify-between'>
<div className='block'>
<img src='/assets/images/login-banner.png' />
<div className='pt-4' dir='rtl'>
{/* // TODO: add persian digits font */}
<h6 className='text-lg font-bold'>کد فعالسازی را وارد کنید</h6>
<p className='mt-3 text-[13px]'>کد ۶ رقمی ارسال شده به شماره {phoneNumber} را وارد نمایید.</p>
</div>
<OTPInputField
className='mt-10'
maxLength={6}
htmlFor='otp'
labelText=''
value={value}
placeholder=''
onChange={onChange}
type='text'>
</OTPInputField>
<div className='inline-flex justify-center items-center w-full pt-6 text-xs'>
<div dir='rtl'>
{timerRunning ?
<div>{secondsLeft} ثانیه دیگر تا دریافت کد</div> :
<div>
کد را دریافت نکردید؟
<span className='text-primary px-1'>
<button id={AUTH_PAGE_ELEMENTS.RESEND_OTP} className='cursor-pointer' onClick={onClick}>
دوباره امتحان کنید
</button>
</span>
</div>
}
</div>
</div>
</div>
<PrimaryButton type='submit'>بعدی</PrimaryButton>
</form>
)
}
export default StepEnterOtp
@@ -0,0 +1,51 @@
import PrimaryButton from '@/components/button/PrimaryButton';
import { EyeToggleIcon } from '@/components/icons/EyeToggleIcon';
import InputField from '@/components/input/InputField';
import { AUTH_PAGE_ELEMENT } from '@/enums';
import React from 'react'
type Props = {
onSubmit: React.FormEventHandler<HTMLFormElement> | undefined;
onChange: ((e: React.ChangeEvent<HTMLInputElement>) => void) & React.ChangeEventHandler<HTMLInputElement>;
onClick: React.MouseEventHandler<HTMLButtonElement> | undefined;
value: string;
passwordVisible: boolean;
rememberMe: boolean;
}
function StepEnterPassword({ onSubmit, onChange, onClick, value, passwordVisible, rememberMe }: Props) {
return (
<form onSubmit={onSubmit} className='p-6 h-full flex flex-col justify-between'>
<div className='block'>
<img src='/assets/images/login-banner.png' />
<div className='pt-4' dir='rtl'>
<h6 className='text-lg font-bold'>ورود به سیستم</h6>
<p className='mt-3 text-[13px]'>کلمه عبور خود را وارد نمایید.</p>
</div>
<InputField
className='mt-10'
htmlFor='password'
labelText='کلمه عبور'
value={value}
placeholder=''
onChange={onChange}
type={passwordVisible ? 'text' : 'password'}>
<button className='ps-3' id={AUTH_PAGE_ELEMENT.TOGGLE_PASSWORD} type='button' onClick={onClick}>
<EyeToggleIcon slash={passwordVisible} className="pointer-events-none" />
</button>
</InputField>
<div className='inline-flex justify-between items-center w-full pt-3'>
<button id={AUTH_PAGE_ELEMENT.RESET_PASSWORD} type='button' onClick={onClick} className='text-sm2 cursor-pointer'>بازیابی کلمه عبور</button>
<div className='inline-flex justify-between items-center'>
{/* TODO: customize checkbox */}
<label htmlFor='rememberMe' className='text-sm2 px-2 py-0.5'>مرا به خاطر بسپار</label>
<input id='rememberMe' className='h-4.5 w-4.5 checked:accent-primary' onChange={onChange} type='checkbox' checked={rememberMe} /> </div>
</div>
</div>
<PrimaryButton type='submit'>ورود به منو</PrimaryButton>
</form>
)
}
export default StepEnterPassword
+51
View File
@@ -0,0 +1,51 @@
import { useEffect, useRef, useState } from 'react';
export const useCountdown = (shouldStart: boolean, duration: number = 30, delay: number = 1000) => {
const [timerRunning, setTimerRunning] = useState(false);
const [secondsLeft, setSecondsLeft] = useState(duration);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (shouldStart) {
setSecondsLeft(duration);
setTimerRunning(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldStart]);
const stop = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
const restart = () => {
stop();
setSecondsLeft(duration);
setTimerRunning(true);
}
useEffect(() => {
if (timerRunning && intervalRef.current === null) {
intervalRef.current = setInterval(() => {
setSecondsLeft((prev) => {
if (prev <= 1) {
setTimerRunning(false);
clearInterval(intervalRef.current!);
intervalRef.current = null;
return 0;
}
return prev - 1;
});
}, delay);
}
return () => {
stop();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timerRunning]);
return { timerRunning, secondsLeft, setTimerRunning, setSecondsLeft, stop, restart };
};