Skip to content
This repository was archived by the owner on Jan 3, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions client/src/components/layouts/auth/DetailsCollection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import useAuthStore from "@/stores/useAuthStore";
import useUIStore from "@/stores/useUIStore";
import { cn, RESPONSE_MESSAGE } from "@/lib/utils";

const DetailsCollection = () => {
const navigate = useNavigate();
const [dots, setDots] = useState('');

const usernameRef = useRef<HTMLInputElement | null>(null);
const passwordRef = useRef<HTMLInputElement | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);

const username = useAuthStore((state) => state.username);
const password = useAuthStore((state) => state.password);
const setUsername = useAuthStore((state) => state.setUsername);
const setPassword = useAuthStore((state) => state.setPassword);
const resetAuth = useAuthStore((state) => state.resetAuth);

const loading = useUIStore((state) => state.loading);
const setLoading = useUIStore((state) => state.setLoading);
const setSuccess = useUIStore((state) => state.setSuccess);
const setError = useUIStore((state) => state.setError);

useEffect(() => {
if (!loading) {
setDots('');
return;
}

const interval = setInterval(() => {
setDots(prev => (prev.length >= 3 ? '' : prev + '.'));
}, 400);

return () => clearInterval(interval);
}, [loading]);

useEffect(() => {
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === "Enter" && document.activeElement !== usernameRef.current && document.activeElement !== passwordRef.current && !loading) {
e.preventDefault();
usernameRef.current?.form?.requestSubmit();
}
};

window.addEventListener("keydown", handleKeydown);

return () => window.removeEventListener("keydown", handleKeydown);
}, [loading]);

useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, []);

const handleSubmit = useCallback(async (e: React.FormEvent) => {
e.preventDefault();
if (loading || !username || !password) return;

try {
setLoading(true);

// Perform login logic here (e.g. API call)
await new Promise((resolve) => setTimeout(resolve, 2000));
// await Promise.reject(new Error('failed'))

console.log("Username:", username);
console.log("Password:", password)

setSuccess(RESPONSE_MESSAGE.loginSuccess.registered);
navigate('/'); // /onboarding route whenever we're done with that

if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => resetAuth(), 2000);
} catch (error) {
console.error("Verification failed: ", error);

// check error type - api failure, rate-limited, otp-expired (duration: Infinity) etc
setError(RESPONSE_MESSAGE.loginErrors.serverError);
} finally {
setLoading(false);
}
}, [loading, username, password, navigate, setLoading, setSuccess, setError, resetAuth]);

const handleInputChange = useCallback((type: 'username' | 'password') => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;

if (type === 'username') setUsername(value);
else setPassword(value);
}, [setUsername, setPassword]);

return (
<>
<h1 className="flex items-center my-3 text-xl font-medium text-white gap-x-2.5">
<span>Let's</span>
<span>Get</span>
<span>Started</span>
</h1>

<div className="flex flex-col items-center text-white/70 text-sm">
<p>Please enter your name and create a password to begin.</p>
<p>This will help us set up your account.</p>
</div>

<form
onSubmit={handleSubmit}
className="flex flex-col items-center gap-y-px"
>
<div className="flex flex-row items-center mt-4 mb-2 gap-x-4">
<input
id="username"
ref={usernameRef}
autoFocus={true}
disabled={loading}
type="text"
required
minLength={3}
maxLength={15}
value={username}
onChange={handleInputChange('username')}
placeholder="Enter Username"
aria-label="Enter your username"
className={
`w-44 p-2 bg-secondary/65 rounded-lg font-outfit text-lg text-accent/70 text-center
placeholder:text-white/50 placeholder:text-sm placeholder:text-center placeholder:font-geist-mono border
border-transparent focus:border-white/20 focus:border-[0.5px] outline-none transition-all duration-300 ease-in`
}
/>
<input
id="password"
ref={passwordRef}
disabled={loading}
type="password"
required
minLength={8}
maxLength={20}
value={password}
onChange={handleInputChange('password')}
placeholder="Enter Password"
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$"
title="Password must be at least 8 characters long and include an uppercase letter, a lowercase letter, a number, and a symbol."
aria-label="Enter your password"
className={
`w-44 p-2 bg-secondary/65 rounded-lg font-outfit text-lg text-accent/70 text-center
placeholder:text-white/50 placeholder:text-sm placeholder:text-center placeholder:font-geist-mono border
border-transparent focus:border-white/20 focus:border-[0.5px] outline-none transition-all duration-300 ease-in`
}
/>
</div>
<button
type="submit"
disabled={loading || !username || !password}
className={cn(
"bg-secondary/20 text-accent/60 hover:text-accent font-satoshi font-semibold text-xl p-2 mt-4 rounded-xl cursor-pointer transition-all duration-300 ease-in",
loading ? 'text-accent' : 'w-65',
)}
>
<span className="inline-block hover:scale-101 transition-transform duration-300 ease-in">
{loading ? `Creating Your Account${dots}` : 'Get Started'}
</span>
</button>
</form>
</>
)
}

export default DetailsCollection;
153 changes: 153 additions & 0 deletions client/src/components/layouts/auth/EmailRegistration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { useEffect, useRef, useState, useCallback } from "react";
import { Link } from "react-router-dom";
import useUIStore from "@/stores/useUIStore";
import useAuthStore from "@/stores/useAuthStore";
import { cn, RESPONSE_MESSAGE } from "@/lib/utils";

const EmailRegistration = () => {
const [dots, setDots] = useState('');
const inputRef = useRef<HTMLInputElement | null>(null);

const loading = useUIStore((state) => state.loading);
const setLoading = useUIStore((state) => state.setLoading);
const setSuccess = useUIStore((state) => state.setSuccess);
const setError = useUIStore((state) => state.setError);

const emailUsername = useAuthStore((state) => state.emailUsername);
const setEmailUsername = useAuthStore((state) => state.setEmailUsername);
const setEmail = useAuthStore((state) => state.setEmail);
const setStep = useAuthStore((state) => state.setStep);

useEffect(() => {
if (!loading) {
setDots('');
return;
}

const interval = setInterval(() => {
setDots(prev => (prev.length >= 3 ? '' : prev + '.'));
}, 400);

return () => clearInterval(interval);
}, [loading]);

useEffect(() => {
const handleKeydown = (e: KeyboardEvent) => {
const isTypingKey = (e.key?.length === 1 || e.key === "Backspace") && e.key !== "@";

if (inputRef.current && document.activeElement !== inputRef.current && isTypingKey) {
inputRef.current?.focus();
return;
}

if (e.key === "Enter" && document.activeElement !== inputRef.current && inputRef.current?.validity.valid && !loading) inputRef.current.form?.requestSubmit();
};

window.addEventListener("keydown", handleKeydown);

return () => window.removeEventListener("keydown", handleKeydown);
}, [loading]);

const handleSubmit = useCallback(async (e: React.FormEvent) => {
e.preventDefault();
if (loading) return;

try {
setLoading(true);

const cleanedEmailUsername = emailUsername.trim().split('@')[0];
const cleanedUsername = cleanedEmailUsername.replace(/\d*\..*$/, '');
const fullEmail = `${cleanedEmailUsername}@learner.manipal.edu`;

setEmailUsername(cleanedEmailUsername);
setEmail(fullEmail);

// Perform login logic here (e.g. API call)
await new Promise((resolve) => setTimeout(resolve, 2000));
// await Promise.reject(new Error('failed'))

console.log("Verifying with Email: ", fullEmail);
console.log("Cleaned Username: ", cleanedUsername);

setSuccess(RESPONSE_MESSAGE.otpSuccess.sent);
setStep('otp');
} catch (error) {
console.error("Verification failed: ", error);

// check error type - api failure, rate-limited, otp-expired (duration: Infinity) etc
setError(RESPONSE_MESSAGE.otpErrors.serverError);
} finally {
setLoading(false);
}
}, [loading, emailUsername, setLoading, setEmailUsername, setEmail, setStep, setSuccess, setError]);

const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setEmailUsername(e.target.value.toLowerCase());
}, []);

const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "@") {
e.preventDefault();
inputRef.current?.blur();
}
}, []);

return (
<>
<h1 className="my-5 text-xl font-medium text-white">
What's your Outlook email address?
</h1>
<form
onSubmit={handleSubmit}
className="flex flex-col items-center gap-y-0.5"
noValidate={false}
>
<div className="flex items-center">
<input
id="email"
ref={inputRef}
autoFocus={true}
disabled={loading}
type="text"
required
pattern={import.meta.env.VITE_MIT_EMAIL_REGEX}
title="Use format: yourname.mitmpl20xx or yourname.mitblr20xx"
value={emailUsername}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Enter your email address"
className={cn(
`w-44 p-2 bg-secondary/65 rounded-lg font-geist-sans text-[15px] text-white
placeholder:text-white/50 placeholder:text-[14px] placeholder:text-center placeholder:font-outfit border border-transparent
focus:border-white/20 focus:border-[0.5px] outline-none transition-all duration-300 ease-in`,
emailUsername ? 'text-center' : 'text-left'
)}
/>
<p className="pl-2 text-white/50 text-sm">
@learner.manipal.edu
</p>
</div>
<button
type="submit"
disabled={loading}
className="bg-secondary/20 text-accent font-jetbrains-mono tracking-tighter p-2 mt-4 w-80 rounded-md cursor-pointer"
>
{loading ? `Verifying${dots}` : 'Verify'}
</button>
</form>
<div className="m-4">
<span className="text-white/80">
Already have an account?
</span>
<Link
to={'/auth/login'}
className="pl-2 font-jetbrains-mono text-accent/65 font-medium text-sm tracking-tighter"
>
Log In
</Link>
</div>
</>
);
}

export default EmailRegistration
Loading