feat(auth): implement email/password login form

This commit is contained in:
dhinesh-m24
2026-01-28 15:33:05 +05:30
parent 959a8c41e9
commit 6e81a062ab
18 changed files with 4287 additions and 8 deletions

View File

@@ -1,11 +1,28 @@
import React from 'react';
import { Provider } from 'react-redux';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import AppRoutes from './routes';
import { store } from './store/store';
import { initializeAuthPersistence } from './services/authService';
// Initialize the QueryClient
const queryClient = new QueryClient();
function App() {
// Initialize Firebase Auth persistence
initializeAuthPersistence();
/**
* Root Application Component.
* Wraps the app with Redux Provider and React Query Provider.
*/
const App: React.FC = () => {
return (
<div className="bg-black">
<h1 className="text-white">Hello World</h1>
</div>
)
}
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<AppRoutes />
</QueryClientProvider>
</Provider>
);
};
export default App
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,40 @@
import * as React from "react"
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
size?: "default" | "sm" | "lg" | "icon"
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "default", size = "default", ...props }, ref) => {
const baseStyles = "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
const variants = {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
}
const sizes = {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3 text-xs",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
}
return (
<button
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className || ''}`}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button }

View File

@@ -0,0 +1,18 @@
import * as React from "react"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => (
<input
type={type}
className={`flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm transition-colors ${className || ''}`}
ref={ref}
{...props}
/>
)
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,16 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={`text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 ${className || ''}`}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@@ -0,0 +1,329 @@
import React, { useState, useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { Loader2, AlertCircle } from "lucide-react";
import loginHero from "../../assets/login-hero.png";
import logo from "../../assets/logo.png";
import { Label } from "@radix-ui/react-label";
import { Input } from "../../common/components/ui/input";
import { Button } from "../../common/components/ui/button";
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
import type { User } from "firebase/auth";
import { app as firebaseApp } from "../../services/authService";
import { FirebaseError } from "firebase/app";
/**
* Login Page Component.
* Features a modern split-screen layout with an inspirational hero image.
* Handles user authentication via email/password with client-side validation.
*/
type LocationState = {
from?: {
pathname: string;
};
};
const Login: React.FC = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [emailError, setEmailError] = useState("");
const [passwordError, setPasswordError] = useState("");
const [isFormValid, setIsFormValid] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [user, setUser] = useState<User | null>(null);
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
setIsFormValid(validateForm(email, password));
}, [email, password]);
// Validate email format
const validateEmail = (value: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
};
// Validate password length
const validatePassword = (value: string): boolean => {
return value.length >= 8;
};
// Validate entire form
const validateForm = (emailValue: string, passwordValue: string): boolean => {
return validateEmail(emailValue) && validatePassword(passwordValue);
};
// Handle email input change
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setEmail(value);
if (value.trim() && !validateEmail(value)) {
setEmailError("Please enter a valid email address");
} else {
setEmailError("");
}
};
// Handle password input change
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setPassword(value);
if (value && !validatePassword(value)) {
setPasswordError("Password must be at least 8 characters");
} else {
setPasswordError("");
}
};
// Navigate user after login (customize as needed)
useEffect(() => {
if (!user) return;
const state = location.state as LocationState | null;
const from = state?.from?.pathname;
navigate(from ?? "/dashboard", { replace: true });
}, [user, navigate, location.state]);
// Clear error message when component unmounts
useEffect(() => {
return () => {
setError("");
};
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
// Validate before submission
if (!isFormValid) {
if (!validateEmail(email)) {
setEmailError("Please enter a valid email address");
}
if (!validatePassword(password)) {
setPasswordError("Password must be at least 8 characters");
}
return;
}
setIsLoading(true);
try {
const auth = getAuth(firebaseApp);
const userCredential = await signInWithEmailAndPassword(
auth,
email,
password,
);
setUser(userCredential.user);
} catch (err: unknown) {
let message = "Failed to sign in. Please try again.";
if (err instanceof FirebaseError) {
switch (err.code) {
case "auth/user-not-found":
case "auth/wrong-password":
message = "Invalid email or password.";
break;
case "auth/too-many-requests":
message = "Too many failed attempts. Please try again later.";
break;
default:
message = err.message;
}
} else if (err instanceof Error) {
message = err.message;
}
setError(message);
} finally {
setIsLoading(false);
}
};
return (
<div className="flex min-h-screen bg-slate-50/50">
{/* Left Side: Hero Image (Hidden on Mobile) */}
<div className="hidden lg:flex lg:w-3/5 xl:w-[65%] relative overflow-hidden">
<img
src={loginHero}
alt="Modern workspace"
className="absolute inset-0 w-full h-full object-cover transform scale-105 hover:scale-100 transition-transform duration-10000 ease-in-out"
/>
{/* Cinematic Overlay */}
<div className="absolute inset-0 bg-gradient-to-r from-black/60 via-black/20 to-transparent z-10" />
<div className="absolute inset-0 bg-primary/10 backdrop-blur-[2px] z-0" />
{/* Top Left Logo */}
<div className="absolute top-12 left-12 z-20">
<img
src={logo}
alt="Krow Logo"
className="h-10 w-auto brightness-0 invert"
/>
</div>
<div className="absolute bottom-12 left-12 right-12 text-white z-10">
<h1 className="text-5xl font-bold mb-4 tracking-tight leading-tight">
Streamline your workforce <br /> with{" "}
<span className="text-secondary underline decoration-4 underline-offset-8">
KROW
</span>
</h1>
<p className="text-lg text-slate-100/90 max-w-lg leading-relaxed">
The all-in-one platform for managing staff, orders, and professional
relationships with precision and ease.
</p>
</div>
</div>
{/* Right Side: Login Form */}
<div className="w-full lg:w-2/5 xl:w-[35%] flex flex-col justify-center items-center p-8 sm:p-12 md:p-16 lg:p-20 bg-white border-l border-slate-200 z-10">
<div className="w-full max-w-md space-y-8">
{/* Logo / Branding */}
<div className="flex flex-col items-center lg:items-start space-y-4">
<img
src={logo}
alt="Krow Logo"
className="h-10 w-auto lg:hidden mb-2"
/>
<div className="space-y-1">
<h2 className="text-3xl font-bold tracking-tight text-primary-text">
Welcome Back
</h2>
<p className="text-secondary-text font-medium">
Please enter your details to sign in
</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="flex items-center p-4 text-sm text-destructive-foreground bg-destructive/70 border border-destructive/20 rounded-xl transition-all animate-in fade-in slide-in-from-top-2">
<AlertCircle className="w-4 h-4 mr-2 flex-shrink-0" />
<span>{error}</span>
</div>
)}
<div className="space-y-2">
<Label htmlFor="email" className="text-sm font-semibold">
Work Email
</Label>
<Input
id="email"
type="email"
placeholder="name@company.com"
value={email}
onChange={handleEmailChange}
disabled={isLoading}
required
aria-describedby={emailError ? "email-error" : undefined}
className={
emailError
? "border-destructive focus-visible:ring-destructive"
: ""
}
/>
{emailError && (
<p
id="email-error"
className="text-xs text-destructive font-medium"
>
{emailError}
</p>
)}
</div>
<div className="space-y-2">
<div className="flex items-center justify-between mb-1">
<Label htmlFor="password" className="text-sm font-semibold">
Password
</Label>
<button
type="button"
className="text-xs font-semibold text-primary hover:underline underline-offset-2"
onClick={() => console.log("TODO: Implement forgot password")}
>
Forgot password?
</button>
</div>
<Input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={handlePasswordChange}
disabled={isLoading}
required
aria-describedby={passwordError ? "password-error" : undefined}
className={
passwordError
? "border-destructive focus-visible:ring-destructive"
: ""
}
/>
{passwordError && (
<p
id="password-error"
className="text-xs text-destructive font-medium"
>
{passwordError}
</p>
)}
</div>
<Button
type="submit"
className="w-full"
disabled={isLoading || !isFormValid}
size="default"
>
{isLoading ? (
<>
<Loader2 className="w-5 h-5 mr-3 animate-spin" />
Authenticating...
</>
) : (
"Sign In to Workspace"
)}
</Button>
</form>
{/* Helper for MVP Testing / Demo purposes */}
<div className="mt-8 p-6 bg-slate-50/80 rounded-2xl border border-slate-100 backdrop-blur-sm">
<p className="text-[11px] font-bold text-muted-text uppercase tracking-widest mb-4">
Quick Login (Development)
</p>
<div className="flex flex-wrap gap-2">
{[
{ label: "Admin", email: "admin@krow.com" },
{ label: "Client", email: "client@krow.com" },
{ label: "Vendor", email: "vendor@krow.com" },
].map((cred) => (
<button
key={cred.label}
type="button"
onClick={() => {
setEmail(cred.email);
setPassword("password");
}}
className="px-4 py-2 bg-white border border-slate-200 text-[11px] font-bold rounded-xl hover:border-primary hover:text-primary transition-all"
>
{cred.label}
</button>
))}
</div>
</div>
</div>
</div>
</div>
);
};
export default Login;

View File

@@ -0,0 +1,141 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
import { loginWithEmail, logout, getCurrentUser } from "../../services/authService";
import type { User } from "firebase/auth";
export interface AuthUser {
uid: string;
email: string | null;
displayName: string | null;
photoURL: string | null;
role?: string;
}
interface AuthState {
user: AuthUser | null;
isAuthenticated: boolean;
status: "idle" | "loading" | "succeeded" | "failed";
error: string | null;
}
const initialState: AuthState = {
user: null,
isAuthenticated: false,
status: "idle",
error: null,
};
/**
* Async thunk for user login
*/
export const loginUser = createAsyncThunk(
"auth/loginUser",
async ({ email, password }: { email: string; password: string }, { rejectWithValue }) => {
const result = await loginWithEmail(email, password);
if (!result.success) {
return rejectWithValue(result.error);
}
const firebaseUser = result.user as User;
return {
uid: firebaseUser.uid,
email: firebaseUser.email,
displayName: firebaseUser.displayName,
photoURL: firebaseUser.photoURL,
};
}
);
/**
* Async thunk for user logout
*/
export const logoutUser = createAsyncThunk("auth/logoutUser", async (_, { rejectWithValue }) => {
const result = await logout();
if (!result.success) {
return rejectWithValue(result.error);
}
return null;
});
/**
* Async thunk to check if user is already logged in
*/
export const checkAuthStatus = createAsyncThunk("auth/checkAuthStatus", async () => {
const currentUser = getCurrentUser();
if (currentUser) {
return {
uid: currentUser.uid,
email: currentUser.email,
displayName: currentUser.displayName,
photoURL: currentUser.photoURL,
};
}
return null;
});
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
clearError: (state) => {
state.error = null;
},
setRole: (state, action: PayloadAction<string>) => {
if (state.user) {
state.user.role = action.payload;
}
},
},
extraReducers: (builder) => {
// Login thunk
builder
.addCase(loginUser.pending, (state) => {
state.status = "loading";
state.error = null;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.status = "succeeded";
state.isAuthenticated = true;
state.user = action.payload;
state.error = null;
})
.addCase(loginUser.rejected, (state, action) => {
state.status = "failed";
state.error = action.payload as string;
state.isAuthenticated = false;
});
// Logout thunk
builder
.addCase(logoutUser.fulfilled, (state) => {
state.user = null;
state.isAuthenticated = false;
state.status = "idle";
state.error = null;
})
.addCase(logoutUser.rejected, (state, action) => {
state.error = action.payload as string;
});
// Check auth status thunk
builder
.addCase(checkAuthStatus.fulfilled, (state, action) => {
if (action.payload) {
state.user = action.payload;
state.isAuthenticated = true;
} else {
state.user = null;
state.isAuthenticated = false;
}
state.status = "idle";
});
},
});
export const { clearError, setRole } = authSlice.actions;
export default authSlice.reducer;

View File

@@ -0,0 +1,23 @@
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
import { getAuth } from "firebase/auth";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID
};
// Initialize Firebase
export const app = initializeApp(firebaseConfig);
export const analytics = getAnalytics(app);
export const auth = getAuth(app);

View File

@@ -0,0 +1,8 @@
const Dashboard = () => {
return (
<div>Dashboard</div>
)
}
export default Dashboard

View File

@@ -1 +1,162 @@
@import url('https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&display=swap');
@import "tailwindcss";
@theme {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-primary-text: hsl(var(--text-primary));
--color-secondary-text: hsl(var(--text-secondary));
--color-muted-text: hsl(var(--text-muted));
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
}
@layer base {
:root {
--background: 220 20% 98%;
--foreground: 226 30% 12%;
--card: 220 20% 98%;
--card-foreground: 226 30% 10%;
--popover: 220 20% 98%;
--popover-foreground: 226 30% 10%;
--primary: 226 91% 45%;
--primary-foreground: 210 40% 98%;
--secondary: 220 15% 95%;
--secondary-foreground: 226 30% 25%;
--muted: 220 15% 95%;
--muted-foreground: 220 9% 45%;
--accent: 53 94% 62%;
--accent-foreground: 53 94% 15%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 220 15% 90%;
--input: 220 15% 90%;
--ring: 226 91% 45%;
--radius: 0.75rem;
--text-primary: 226 30% 12%;
--text-secondary: 226 20% 35%;
--text-muted: 226 15% 55%;
}
.dark {
--background: 226 30% 5%;
--foreground: 210 40% 98%;
--card: 226 30% 7%;
--card-foreground: 210 40% 98%;
--popover: 226 30% 5%;
--popover-foreground: 210 40% 98%;
--primary: 226 91% 55%;
--primary-foreground: 220 20% 10%;
--secondary: 226 30% 15%;
--secondary-foreground: 210 40% 98%;
--muted: 226 30% 15%;
--muted-foreground: 220 9% 65%;
--accent: 53 94% 62%;
--accent-foreground: 53 94% 15%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 226 30% 15%;
--input: 226 30% 15%;
--ring: 226 91% 55%;
--text-primary: 210 40% 98%;
--text-secondary: 220 9% 80%;
--text-muted: 220 9% 65%;
}
}
@layer base {
* {
border-color: hsl(var(--border));
}
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
font-family: 'Instrument Sans', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer utilities {
.glass {
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.dark .glass {
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.animate-in {
animation: fade-in 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.animate-in-slide-up {
animation: slide-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
.transition-premium {
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

13
apps/web/src/lib/utils.ts Normal file
View File

@@ -0,0 +1,13 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
/**
* Utility function to merge Tailwind CSS classes conditionally.
* Combines clsx for conditional logic and tailwind-merge to handle conflicts.
*
* @param inputs - List of class values (strings, objects, arrays)
* @returns Merged class string
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

30
apps/web/src/routes.tsx Normal file
View File

@@ -0,0 +1,30 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Login from './features/auth/Login';
import Dashboard from './features/dashboard/Dashboard';
/**
* AppRoutes Component
* Defines the main routing structure of the application.
* Groups routes by Layout (Public vs App).
*/
const AppRoutes: React.FC = () => {
return (
<Router>
<Routes>
{/* Public Routes */}
<Route
path="/login"
element={
<Login />
}
/>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
</Router>
);
};
export default AppRoutes;

View File

@@ -0,0 +1,106 @@
import {
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
setPersistence,
browserLocalPersistence,
sendPasswordResetEmail,
} from "firebase/auth";
import type { User, AuthError } from "firebase/auth";
import { app} from "../features/auth/firebase"
import { getAuth } from "firebase/auth";
const auth = getAuth(app);
/**
* Initialize Firebase Auth persistence
* Ensures user session persists across page refreshes
*/
export const initializeAuthPersistence = async () => {
try {
await setPersistence(auth, browserLocalPersistence);
} catch (error) {
console.error("Error setting auth persistence:", error);
}
};
/**
* Login user with email and password
*/
export const loginWithEmail = async (email: string, password: string) => {
try {
const userCredential = await signInWithEmailAndPassword(auth, email, password);
return {
success: true,
user: userCredential.user,
error: null,
};
} catch (error) {
const authError = error as AuthError;
return {
success: false,
user: null,
error: getAuthErrorMessage(authError.code),
};
}
};
/**
* Sign out the current user
*/
export const logout = async () => {
try {
await signOut(auth);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
};
/**
* Send password reset email
*/
export const sendPasswordReset = async (email: string) => {
try {
await sendPasswordResetEmail(auth, email);
return { success: true };
} catch (error) {
const authError = error as AuthError;
return { success: false, error: getAuthErrorMessage(authError.code) };
}
};
/**
* Subscribe to auth state changes
* Returns unsubscribe function
*/
export const subscribeToAuthState = (callback: (user: User | null) => void) => {
return onAuthStateChanged(auth, callback);
};
/**
* Get current user synchronously
*/
export const getCurrentUser = () => {
return auth.currentUser;
};
/**
* Convert Firebase error codes to user-friendly messages
*/
const getAuthErrorMessage = (errorCode: string): string => {
const errorMessages: Record<string, string> = {
"auth/invalid-email": "Invalid email address format.",
"auth/user-disabled": "This user account has been disabled.",
"auth/user-not-found": "Invalid email or password.",
"auth/wrong-password": "Invalid email or password.",
"auth/invalid-credential": "Invalid email or password.",
"auth/too-many-requests": "Too many login attempts. Please try again later.",
"auth/operation-not-allowed": "Login is currently disabled. Please try again later.",
"auth/network-request-failed": "Network error. Please check your connection.",
};
return errorMessages[errorCode] || "An error occurred during login. Please try again.";
};
export { app };

View File

@@ -0,0 +1,15 @@
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "../features/auth/authSlice";
/**
* Redux Store Configuration
* Centralizes all state management for the application
*/
export const store = configureStore({
reducer: {
auth: authReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

View File

@@ -0,0 +1,40 @@
/**
* Authentication Type Definitions
* Defines types for auth-related operations and state management
*/
export interface LoginCredentials {
email: string;
password: string;
}
export interface AuthResponse {
success: boolean;
user: AuthUser | null;
error: string | null;
}
export interface AuthUser {
uid: string;
email: string | null;
displayName: string | null;
photoURL: string | null;
role?: string;
}
export interface AuthState {
user: AuthUser | null;
isAuthenticated: boolean;
status: "idle" | "loading" | "succeeded" | "failed";
error: string | null;
}
export interface PasswordResetResponse {
success: boolean;
error?: string;
}
export interface LogoutResponse {
success: boolean;
error?: string;
}