feat(auth): implement new login page with animated auth card and forgot password modal

- Add new LoginPage with responsive 2-column layout
- Create reusable AuthCard components (AuthCard, AuthInput, AuthButton)
- Implement animated AuthFormCard for Sign In/Register toggle
- Add ForgotPasswordModal with OTP verification flow
- Add background decorations (logo glow, waves, gradients)
- Remove old sign-in page, set LoginPage as root route
- Add SVG type declarations for Vite
This commit is contained in:
SanaullasAzaan 2025-12-23 20:04:38 +05:30
parent 5fa95db946
commit 52d4aaa88b
24 changed files with 1728 additions and 59 deletions

49
package-lock.json generated
View File

@ -15,6 +15,7 @@
"@tanstack/router-devtools": "^1.143.2", "@tanstack/router-devtools": "^1.143.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.23.26",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -3012,6 +3013,33 @@
"url": "https://github.com/sponsors/rawify" "url": "https://github.com/sponsors/rawify"
} }
}, },
"node_modules/framer-motion": {
"version": "12.23.26",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
"integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -3614,6 +3642,21 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -4147,6 +4190,12 @@
"typescript": ">=4.8.4" "typescript": ">=4.8.4"
} }
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@ -17,6 +17,7 @@
"@tanstack/router-devtools": "^1.143.2", "@tanstack/router-devtools": "^1.143.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.23.26",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",

View File

@ -0,0 +1,46 @@
# Images Directory
This directory contains all image assets for the AgenticIQ application, organized by category.
## Folder Structure
```
images/
├── logo/ # Brand logos and identity assets
├── icons/ # UI icons and symbols
├── auth/ # Authentication-related images (social login icons, etc.)
├── backgrounds/ # Background images and decorative elements
└── ui/ # General UI component images
```
## Usage Guidelines
### Importing Images in Components
```typescript
// Example: Importing a logo
import agenticiqLogo from '@/assets/images/logo/agenticiq-logo.png';
// Example: Importing an icon
import googleIcon from '@/assets/images/auth/google-icon.svg';
```
### File Naming Convention
- Use **kebab-case** for all filenames (e.g., `agenticiq-logo.png`, `google-icon.svg`)
- Be descriptive and avoid abbreviations
- Include size/resolution suffix if multiple versions exist (e.g., `logo-256x256.png`)
### Image Formats
- **Logos**: PNG (with transparency) or SVG
- **Icons**: SVG (preferred) or PNG
- **Photos**: WebP (preferred) or JPG
- **Backgrounds**: WebP or PNG
### Optimization
- Optimize images before adding them to this directory
- Use appropriate formats for the use case
- Consider responsive images for different screen densities

View File

@ -0,0 +1,35 @@
# Authentication Assets
This directory contains images related to authentication and social login.
## Files to Add
### Social Login Icons
- `google-icon.svg` - Google sign-in button icon
- `azure-icon.svg` - Microsoft Azure sign-in button icon
- `github-icon.svg` - GitHub sign-in icon (if needed)
- `facebook-icon.svg` - Facebook sign-in icon (if needed)
### Authentication UI
- `auth-background.svg` - Background pattern for auth pages (if needed)
- `lock-icon.svg` - Security/lock icon
- `user-icon.svg` - User profile icon
## Usage Example
```typescript
import googleIcon from '@/assets/images/auth/google-icon.svg';
import azureIcon from '@/assets/images/auth/azure-icon.svg';
<button>
<img src={googleIcon} alt="Google" className="w-6 h-6" />
<span>Sign In with Google</span>
</button>
```
## Icon Specifications
- **Size**: 24x24px or 32x32px for social login buttons
- **Format**: SVG preferred for scalability
- **Style**: Match brand guidelines for each provider

View File

@ -0,0 +1,32 @@
# Background Assets
This directory contains background images and decorative elements.
## Files to Add
### Decorative Elements
- `gradient-blob-1.svg` - Decorative gradient shape (if needed as image)
- `gradient-blob-2.svg` - Additional decorative element
- `pattern-overlay.svg` - Pattern overlay (if needed)
### Background Images
- `auth-background.jpg` or `.webp` - Full background image for auth pages (if needed)
- `dashboard-background.svg` - Dashboard background pattern
## Usage Example
```typescript
import backgroundPattern from '@/assets/images/backgrounds/pattern-overlay.svg';
<div
style={{ backgroundImage: `url(${backgroundPattern})` }}
className="absolute inset-0 opacity-20"
/>
```
## Notes
- Most decorative backgrounds are created with CSS gradients (as in login-page.tsx)
- Only add image files if CSS gradients cannot achieve the desired effect
- Optimize all background images for performance

View File

@ -0,0 +1,10 @@
<svg width="411" height="449" viewBox="0 0 411 449" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M-36.0004 0L317.405 172.143C376.454 203.681 411.924 260.629 410.432 321.505L410.432 614H-36.0004V0Z" fill="url(#paint0_linear_11_38052)"/>
<defs>
<linearGradient id="paint0_linear_11_38052" x1="227.514" y1="-4.05561" x2="223.001" y2="663.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E2E0"/>
<stop offset="0.615385" stop-color="#0033FF"/>
<stop offset="0.904576" stop-color="white"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 536 B

View File

@ -0,0 +1,9 @@
<svg width="527" height="593" viewBox="0 0 527 593" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0L417.142 203.263C486.841 240.503 528.707 307.746 526.947 379.628L526.947 725H0V0Z" fill="url(#paint0_linear_11_38051)"/>
<defs>
<linearGradient id="paint0_linear_11_38051" x1="243.5" y1="60.5" x2="263" y2="927" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E2E0"/>
<stop offset="0.796851" stop-color="white"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 463 B

View File

@ -0,0 +1,12 @@
<svg width="405" height="243" viewBox="0 0 405 243" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M-18.8586 -296.056C-171.92 -235.336 -241.739 -70.7007 -174.805 71.6671C-107.871 214.035 70.4706 280.224 223.532 219.504C376.592 158.784 446.412 -5.85137 379.478 -148.219C312.544 -290.587 134.202 -356.776 -18.8586 -296.056Z" fill="url(#paint0_radial_11_38040)"/>
<defs>
<radialGradient id="paint0_radial_11_38040" cx="0" cy="0" r="1" gradientTransform="matrix(93.0029 -317.795 343.872 89.0633 102.336 -38.276)" gradientUnits="userSpaceOnUse">
<stop stop-color="#C8FAFF"/>
<stop offset="0.33" stop-color="#00CEE0"/>
<stop offset="0.705031" stop-color="#6172F3"/>
<stop offset="0.816395" stop-color="#C6D7FE"/>
<stop offset="1" stop-color="#F5F8FF"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 790 B

View File

@ -0,0 +1,9 @@
<svg width="302" height="397" viewBox="0 0 302 397" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M181.364 -142.056C28.3031 -81.3358 -41.5164 83.2993 25.4178 225.667C92.352 368.035 270.693 434.224 423.754 373.504C576.815 312.784 646.635 148.149 579.7 5.78083C512.766 -136.587 334.425 -202.776 181.364 -142.056Z" fill="url(#paint0_radial_11_38047)"/>
<defs>
<radialGradient id="paint0_radial_11_38047" cx="0" cy="0" r="1" gradientTransform="matrix(140.333 -462.353 501.033 134.576 255.229 260.282)" gradientUnits="userSpaceOnUse">
<stop stop-color="#00CEE0"/>
<stop offset="0.864679" stop-color="#0033FF"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 650 B

View File

@ -0,0 +1,33 @@
# Icon Assets
This directory contains UI icons and symbols used throughout the application.
## Files to Add
### Navigation Icons
- `home-icon.svg`
- `dashboard-icon.svg`
- `settings-icon.svg`
### Action Icons
- `eye-icon.svg` - Show/hide password
- `eye-close-icon.svg` - Hide password
- `search-icon.svg`
- `plus-icon.svg`
- `edit-icon.svg`
- `delete-icon.svg`
### Status Icons
- `success-icon.svg`
- `error-icon.svg`
- `warning-icon.svg`
- `info-icon.svg`
## Usage Example
```typescript
import eyeIcon from '@/assets/images/icons/eye-icon.svg';
<img src={eyeIcon} alt="Show password" className="w-5 h-5" />
```

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -0,0 +1,27 @@
# Logo Assets
This directory contains brand logos and identity assets for AgenticIQ.
## Files to Add
### Primary Logo
- `agenticiq-logo.png` or `agenticiq-logo.svg` - Main AgenticIQ logo
- `agenticiq-logo-white.png` - White variant for dark backgrounds
- `agenticiq-logo-dark.png` - Dark variant for light backgrounds
### Logo Variants
- `agenticiq-logo-horizontal.svg` - Horizontal layout variant
- `agenticiq-logo-vertical.svg` - Vertical/stacked layout variant
- `agenticiq-icon.svg` - Icon-only version (favicon, app icon)
### Trademark Badge
- `trademark-badge.svg` - TM badge component (if separate from logo)
## Usage Example
```typescript
import agenticiqLogo from '@/assets/images/logo/agenticiq-logo.svg';
<img src={agenticiqLogo} alt="AgenticIQ Logo" />
```

View File

@ -0,0 +1,31 @@
# UI Component Assets
This directory contains images used in UI components throughout the application.
## Files to Add
### Profile & User
- `default-avatar.png` - Default user avatar placeholder
- `profile-placeholder.svg` - Profile image placeholder
### Dashboard Components
- `metric-card-icon.svg` - Icon for metric cards
- `empty-state-illustration.svg` - Empty state illustrations
### General UI
- `loading-spinner.svg` - Loading animation (if not CSS-based)
- `error-illustration.svg` - Error state illustration
- `success-illustration.svg` - Success state illustration
## Usage Example
```typescript
import defaultAvatar from '@/assets/images/ui/default-avatar.png';
<img
src={defaultAvatar}
alt="User avatar"
className="w-10 h-10 rounded-full"
/>
```

View File

@ -0,0 +1,219 @@
/**
* AuthButton Component
* @description Reusable button components for authentication forms.
* Includes primary action button and social login buttons.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { type ButtonHTMLAttributes, type ReactNode } from 'react';
/**
* Props for AuthButton component
*/
interface AuthButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Button content */
children: ReactNode;
/** Loading state */
isLoading?: boolean;
/** Full width button */
fullWidth?: boolean;
}
/**
* Props for SocialButton component
*/
interface SocialButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Button text */
children: ReactNode;
/** Icon element to show before text */
icon: ReactNode;
}
/**
* Props for TextButton component
*/
interface TextButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Button text */
children: ReactNode;
}
/**
* Google icon for social login
* @returns {JSX.Element} Google SVG icon
*/
export function GoogleIcon(): JSX.Element {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M22.56 12.25C22.56 11.47 22.49 10.72 22.36 10H12V14.26H17.92C17.66 15.63 16.88 16.79 15.71 17.57V20.34H19.28C21.36 18.42 22.56 15.6 22.56 12.25Z"
fill="#4285F4"
/>
<path
d="M12 23C14.97 23 17.46 22.02 19.28 20.34L15.71 17.57C14.73 18.23 13.48 18.63 12 18.63C9.13999 18.63 6.70999 16.7 5.83999 14.1H2.17999V16.94C3.98999 20.53 7.69999 23 12 23Z"
fill="#34A853"
/>
<path
d="M5.84 14.1C5.62 13.44 5.49 12.73 5.49 12C5.49 11.27 5.62 10.56 5.84 9.9V7.06H2.18C1.43 8.55 1 10.22 1 12C1 13.78 1.43 15.45 2.18 16.94L5.84 14.1Z"
fill="#FBBC05"
/>
<path
d="M12 5.38C13.62 5.38 15.06 5.94 16.21 7.02L19.36 3.87C17.45 2.09 14.97 1 12 1C7.69999 1 3.98999 3.47 2.17999 7.06L5.83999 9.9C6.70999 7.3 9.13999 5.38 12 5.38Z"
fill="#EA4335"
/>
</svg>
);
}
/**
* Azure icon for social login
* @returns {JSX.Element} Azure SVG icon
*/
export function AzureIcon(): JSX.Element {
return (
<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 11.5L7.667 2.875L14.375 0L14.833 11.5L7.667 20.125L0 11.5Z" fill="#0078D4" />
<path d="M8.625 8.625L14.375 0L23 5.75V17.25L14.375 23L8.625 8.625Z" fill="#0078D4" />
<path d="M8.625 8.625L14.375 11.5L14.375 23L8.625 8.625Z" fill="#0050A4" />
<path d="M14.375 11.5L23 5.75V17.25L14.375 11.5Z" fill="#0050A4" />
</svg>
);
}
/**
* AuthButton component
* @description Primary action button with cyan/teal background.
* Used for main form submissions (Sign In, Sign Up, etc.).
* @param {AuthButtonProps} props - Component props
* @returns {JSX.Element} AuthButton element
*/
export function AuthButton({
children,
isLoading = false,
fullWidth = true,
disabled,
className = '',
...buttonProps
}: AuthButtonProps): JSX.Element {
return (
<button
type="submit"
disabled={disabled || isLoading}
className={`
flex items-center justify-center
h-[52px]
px-[18px] py-[13px]
bg-[#00E2E0]
border border-[rgba(255,255,255,0.2)]
rounded-[12px]
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
text-[16px] font-semibold leading-6 text-black
cursor-pointer
hover:bg-[#00D4D2]
active:bg-[#00C6C4]
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors duration-150
${fullWidth ? 'w-full' : ''}
${className}
`}
{...buttonProps}
>
{isLoading ? (
<span className="flex items-center gap-2">
<svg
className="animate-spin h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Loading...
</span>
) : (
children
)}
</button>
);
}
/**
* SocialButton component
* @description Social login button with white background.
* Used for Google, Azure, and other OAuth providers.
* @param {SocialButtonProps} props - Component props
* @returns {JSX.Element} SocialButton element
*/
export function SocialButton({
children,
icon,
className = '',
...buttonProps
}: SocialButtonProps): JSX.Element {
return (
<button
type="button"
className={`
flex items-center justify-center gap-3
px-4 py-2.5
bg-white
rounded-xl
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
text-sm font-semibold leading-6 text-black
cursor-pointer
hover:bg-gray-50
active:bg-gray-100
transition-colors duration-200
${className}
`}
{...buttonProps}
>
<span className="shrink-0 w-6 h-6 flex items-center justify-center">
{icon}
</span>
{children}
</button>
);
}
/**
* TextButton component
* @description Text-only button for secondary actions (e.g., "Forgot password?").
* @param {TextButtonProps} props - Component props
* @returns {JSX.Element} TextButton element
*/
export function TextButton({
children,
className = '',
...buttonProps
}: TextButtonProps): JSX.Element {
return (
<button
type="button"
className={`
text-sm font-medium leading-5
text-[rgba(255,255,255,0.75)]
cursor-pointer
hover:text-white
hover:underline
transition-colors duration-200
${className}
`}
{...buttonProps}
>
{children}
</button>
);
}

View File

@ -0,0 +1,108 @@
/**
* AuthCard Component
* @description Reusable authentication card with gradient background.
* Can be used for Sign In, Sign Up, Forgot Password, and other auth screens.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { type ReactNode } from 'react';
/**
* Props for AuthCard component
*/
interface AuthCardProps {
/** Child elements to render inside the card */
children: ReactNode;
/** Additional CSS classes for customization */
className?: string;
/** Card variant - 'signin' has standard padding, 'register' has compact padding */
variant?: 'signin' | 'register';
}
/**
* Props for AuthCardHeader component
*/
interface AuthCardHeaderProps {
/** Main title text */
title: string;
/** Subtitle/description text */
subtitle?: string;
}
/**
* Props for AuthCardContent component
*/
interface AuthCardContentProps {
/** Child elements (form fields, etc.) */
children: ReactNode;
/** Additional CSS classes */
className?: string;
}
/**
* AuthCard component
* @description Main container with gradient background (teal to dark blue).
* Provides the card structure for authentication screens.
* No border to avoid dark line artifacts.
* @param {AuthCardProps} props - Component props
* @returns {JSX.Element} AuthCard element
*/
export function AuthCard({ children, className = '', variant = 'signin' }: AuthCardProps): JSX.Element {
const isRegister = variant === 'register';
return (
<div
className={`
flex flex-col items-center gap-8
w-[520px] max-w-full
rounded-[32px]
shadow-[0px_4px_24px_0px_rgba(0,0,0,0.15)]
${isRegister ? 'px-[42px] py-[64px]' : 'px-[60px] py-[80px]'}
${className}
`}
style={{
background: 'linear-gradient(180deg, #00b8b7 0%, #001c8e 100%)',
}}
>
{children}
</div>
);
}
/**
* AuthCardHeader component
* @description Header section with title and optional subtitle.
* @param {AuthCardHeaderProps} props - Component props
* @returns {JSX.Element} AuthCardHeader element
*/
export function AuthCardHeader({ title, subtitle }: AuthCardHeaderProps): JSX.Element {
return (
<div className="flex flex-col items-center w-full">
<div className="flex flex-col items-center gap-2 w-full text-center">
<h2 className="text-[30px] font-semibold leading-[38px] text-white w-full">
{title}
</h2>
{subtitle && (
<p className="text-[18px] font-medium leading-6 text-[rgba(255,255,255,0.75)]">
{subtitle}
</p>
)}
</div>
</div>
);
}
/**
* AuthCardContent component
* @description Content section for form fields and actions.
* @param {AuthCardContentProps} props - Component props
* @returns {JSX.Element} AuthCardContent element
*/
export function AuthCardContent({ children, className = '' }: AuthCardContentProps): JSX.Element {
return (
<div className={`flex flex-col items-center gap-6 w-full rounded-xl ${className}`}>
{children}
</div>
);
}

View File

@ -0,0 +1,311 @@
/**
* AuthFormCard Component
* @description Unified authentication form with animated transitions between Sign In and Register.
* Uses framer-motion for smooth expand/collapse animations.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { useState, type FormEvent } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
AuthCard,
AuthCardHeader,
AuthCardContent,
AuthInput,
AuthButton,
SocialButton,
TextButton,
GoogleIcon,
AzureIcon,
} from './index';
/**
* Auth form mode type
*/
type AuthMode = 'signin' | 'register';
/**
* Props for AuthFormCard component
*/
interface AuthFormCardProps {
/** Initial mode (signin or register) */
initialMode?: AuthMode;
/** Callback when sign in is submitted */
onSignIn?: (email: string, password: string) => void;
/** Callback when register is submitted */
onRegister?: (data: RegisterData) => void;
/** Callback for Google sign in */
onGoogleAuth?: () => void;
/** Callback for Azure sign in */
onAzureAuth?: () => void;
/** Callback for forgot password */
onForgotPassword?: () => void;
}
/**
* Register form data interface
*/
interface RegisterData {
firstName: string;
lastName: string;
email: string;
password: string;
}
/**
* Spring transition for smooth physics-based animations
* Using 'as const' to ensure literal types for framer-motion
*/
const springTransition = {
type: 'spring' as const,
stiffness: 500,
damping: 30,
mass: 1,
};
/**
* Animation variants for the collapsible name fields row
*/
const nameFieldsVariants = {
hidden: {
height: 0,
opacity: 0,
marginBottom: 0,
transition: {
height: { ...springTransition, duration: 0.2 },
opacity: { duration: 0.1 },
marginBottom: { ...springTransition, duration: 0.2 },
},
},
visible: {
height: 'auto',
opacity: 1,
marginBottom: 16,
transition: {
height: { ...springTransition, duration: 0.2 },
opacity: { duration: 0.15, delay: 0.05 },
marginBottom: { ...springTransition, duration: 0.2 },
},
},
};
/**
* Animation variants for forgot password link
*/
const forgotPasswordVariants = {
hidden: {
height: 0,
opacity: 0,
marginTop: 0,
transition: {
height: { ...springTransition, duration: 0.15 },
opacity: { duration: 0.1 },
marginTop: { ...springTransition, duration: 0.15 },
},
},
visible: {
height: 'auto',
opacity: 1,
marginTop: 8,
transition: {
height: { ...springTransition, duration: 0.15 },
opacity: { duration: 0.15, delay: 0.05 },
marginTop: { ...springTransition, duration: 0.15 },
},
},
};
/**
* AuthFormCard component
* @description Unified authentication card with animated transitions between Sign In and Register modes.
* @param {AuthFormCardProps} props - Component props
* @returns {JSX.Element} AuthFormCard element
*/
export function AuthFormCard({
initialMode = 'signin',
onSignIn,
onRegister,
onGoogleAuth,
onAzureAuth,
onForgotPassword,
}: AuthFormCardProps): JSX.Element {
const [mode, setMode] = useState<AuthMode>(initialMode);
const isRegister = mode === 'register';
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
/**
* Handle form submission based on current mode
* @param {FormEvent<HTMLFormElement>} e - Form event
*/
const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
setIsLoading(true);
if (isRegister) {
onRegister?.({ firstName, lastName, email, password });
} else {
onSignIn?.(email, password);
}
setTimeout(() => setIsLoading(false), 1000);
};
/**
* Toggle between Sign In and Register modes
*/
const toggleMode = (): void => {
setMode((prev) => (prev === 'signin' ? 'register' : 'signin'));
setFirstName('');
setLastName('');
setEmail('');
setPassword('');
};
return (
<motion.div
layout
transition={{ layout: { ...springTransition, duration: 0.3 } }}
className="w-[520px] max-w-full"
style={{ willChange: 'transform' }}
>
<AuthCard variant={isRegister ? 'register' : 'signin'}>
{/* Header Section */}
<AuthCardHeader
title={isRegister ? 'Create Your Account' : 'Sign In to Your Account'}
subtitle={
isRegister
? 'Welcome! Please enter your information.'
: 'Welcome back. Please enter your credentials.'
}
/>
{/* Form Content */}
<AuthCardContent>
<form onSubmit={handleSubmit} className="flex flex-col gap-4 w-full">
{/* Collapsible Name Fields Row */}
<AnimatePresence initial={false}>
{isRegister && (
<motion.div
key="name-fields"
variants={nameFieldsVariants}
initial="hidden"
animate="visible"
exit="hidden"
className="overflow-hidden"
style={{ willChange: 'height, opacity' }}
>
<div className="flex gap-3 w-full">
<div className="flex-1 min-w-0">
<AuthInput
label="First Name"
type="text"
placeholder="First name"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
isRequired
autoComplete="given-name"
/>
</div>
<div className="flex-1 min-w-0">
<AuthInput
label="Last Name"
type="text"
placeholder="Last name"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
autoComplete="family-name"
/>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Email Input */}
<motion.div layout transition={springTransition}>
<AuthInput
label="Email"
type="email"
placeholder="Enter your email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
isRequired
autoComplete={isRegister ? 'email' : 'username'}
/>
</motion.div>
{/* Password Input */}
<motion.div layout transition={springTransition}>
<AuthInput
label="Password"
type="password"
placeholder={isRegister ? 'Create a password' : 'Enter your password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
isRequired
autoComplete={isRegister ? 'new-password' : 'current-password'}
/>
</motion.div>
{/* Forgot Password Link - Collapses in Register mode */}
<AnimatePresence initial={false}>
{!isRegister && (
<motion.div
key="forgot-password"
variants={forgotPasswordVariants}
initial="hidden"
animate="visible"
exit="hidden"
className="overflow-hidden flex justify-end w-full"
style={{ willChange: 'height, opacity' }}
>
<TextButton onClick={onForgotPassword}>
Forgot your password?
</TextButton>
</motion.div>
)}
</AnimatePresence>
{/* Submit Button */}
<AuthButton isLoading={isLoading}>
{isRegister ? 'Create Account' : 'Sign In'}
</AuthButton>
</form>
{/* Social Login Buttons */}
<div className="flex gap-3 w-full">
<SocialButton
icon={<GoogleIcon />}
onClick={onGoogleAuth}
className="flex-1"
>
{isRegister ? 'Sign Up with Google' : 'Sign In with Google'}
</SocialButton>
<SocialButton
icon={<AzureIcon />}
onClick={onAzureAuth}
className="flex-1"
>
{isRegister ? 'Sign Up with Azure' : 'Sign In with Azure'}
</SocialButton>
</div>
</AuthCardContent>
{/* Footer Section */}
<div className="flex items-baseline justify-center gap-1 w-full">
<span className="text-sm font-normal leading-5 text-[rgba(255,255,255,0.5)]">
{isRegister ? 'Already have an account?' : "Don't have an account?"}
</span>
<TextButton onClick={toggleMode}>
{isRegister ? 'Sign In' : 'Register here'}
</TextButton>
</div>
</AuthCard>
</motion.div>
);
}

View File

@ -0,0 +1,168 @@
/**
* AuthInput Component
* @description Reusable input field for authentication forms.
* Features semi-transparent background, white text, and password visibility toggle.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { forwardRef, useState, type InputHTMLAttributes } from 'react';
/**
* Props for AuthInput component
*/
interface AuthInputProps extends InputHTMLAttributes<HTMLInputElement> {
/** Label text for the input */
label: string;
/** Whether the field is required */
isRequired?: boolean;
/** Error message to display */
error?: string;
/** Additional container classes */
containerClassName?: string;
}
/**
* Eye closed icon for password visibility toggle
* @returns {JSX.Element} Eye closed SVG icon
*/
function EyeClosedIcon(): JSX.Element {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 2.5L17.5 17.5M8.82 8.82a1.667 1.667 0 002.36 2.36M7.5 14.167A7.5 7.5 0 0110 13.75c2.917 0 5.5 1.25 7.5 3.75a10.833 10.833 0 00-2.358-2.358M12.5 5.833A7.5 7.5 0 0110 6.25C7.083 6.25 4.5 7.5 2.5 10a10.833 10.833 0 002.358 2.358"
stroke="rgba(255,255,255,0.5)"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
/**
* Eye open icon for password visibility toggle
* @returns {JSX.Element} Eye open SVG icon
*/
function EyeOpenIcon(): JSX.Element {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 4.167c-4.167 0-7.5 3.333-7.5 5.833s3.333 5.833 7.5 5.833 7.5-3.333 7.5-5.833-3.333-5.833-7.5-5.833z"
stroke="rgba(255,255,255,0.5)"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10 12.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5z"
stroke="rgba(255,255,255,0.5)"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
/**
* AuthInput component
* @description Styled input field with label, required indicator, and password toggle.
* Semi-transparent background with white text for dark gradient cards.
* @param {AuthInputProps} props - Component props
* @returns {JSX.Element} AuthInput element
*/
export const AuthInput = forwardRef<HTMLInputElement, AuthInputProps>(
function AuthInput(
{
label,
isRequired = false,
error,
containerClassName = '',
type = 'text',
placeholder,
...inputProps
},
ref
): JSX.Element {
const [showPassword, setShowPassword] = useState(false);
const isPasswordType = type === 'password';
const inputType = isPasswordType && showPassword ? 'text' : type;
const togglePasswordVisibility = (): void => {
setShowPassword((prev) => !prev);
};
return (
<div className={`flex flex-col gap-1.5 w-full ${containerClassName}`}>
{/* Label */}
<label className="text-sm font-medium leading-5 text-white">
{label}
{isRequired && <span className="text-[#ff3434] ml-0.5">*</span>}
</label>
{/* Input Container */}
<div
className={`
flex items-center gap-2
w-full px-4 py-2.5
bg-[rgba(255,255,255,0.15)]
border border-[rgba(255,255,255,0.05)]
rounded-xl
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
overflow-hidden
focus-within:border-[rgba(255,255,255,0.3)]
transition-colors duration-200
${error ? 'border-[#ff3434]' : ''}
min-w-0
`}
>
<input
ref={ref}
type={inputType}
className="
flex-1 min-w-0
bg-transparent
text-base font-medium leading-6
text-white
placeholder:text-[rgba(255,255,255,0.5)]
outline-none
border-none
"
placeholder={placeholder}
{...inputProps}
/>
{/* Password Toggle Icon */}
{isPasswordType && (
<button
type="button"
onClick={togglePasswordVisibility}
className="flex-shrink-0 cursor-pointer hover:opacity-80 transition-opacity"
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOpenIcon /> : <EyeClosedIcon />}
</button>
)}
</div>
{/* Error Message */}
{error && (
<p className="text-sm text-[#ff3434] mt-1">{error}</p>
)}
</div>
);
}
);

View File

@ -0,0 +1,391 @@
/**
* ForgotPasswordModal Component
* @description Modal dialog for password recovery with two steps:
* 1. Email input (forgot password)
* 2. OTP verification (verify your email)
* Features smooth animated transitions between steps.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { useState, useRef, useEffect, type FormEvent, type KeyboardEvent, type ClipboardEvent } from 'react';
import { motion, AnimatePresence, type Easing } from 'framer-motion';
import { AuthInput, AuthButton } from './index';
/**
* Modal step type - determines which view to show
*/
type ModalStep = 'email' | 'verify';
/**
* Props for ForgotPasswordModal component
*/
interface ForgotPasswordModalProps {
/** Whether the modal is visible */
isOpen: boolean;
/** Callback when modal should close */
onClose: () => void;
/** Callback when email is submitted */
onSubmit?: (email: string) => void;
/** Callback when OTP is verified */
onVerify?: (otp: string) => void;
}
/**
* Close icon component
*/
function CloseIcon(): JSX.Element {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6L18 18" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/**
* Left arrow icon component
*/
function ArrowLeftIcon(): JSX.Element {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 12H5M5 12L12 19M5 12L12 5" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/**
* Smooth easing curve for animations (cubic-bezier)
*/
const smoothEasing: Easing = [0.4, 0, 0.2, 1];
/**
* OTP Input component for 6-digit verification code
*/
function OTPInput({
otp,
setOtp,
}: {
otp: string[];
setOtp: React.Dispatch<React.SetStateAction<string[]>>;
}): JSX.Element {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
// Auto-focus first input when component mounts
useEffect(() => {
const timer = setTimeout(() => {
inputRefs.current[0]?.focus();
}, 300);
return () => clearTimeout(timer);
}, []);
const handleChange = (index: number, value: string): void => {
if (value.length > 1) value = value.slice(-1);
if (value && !/^\d$/.test(value)) return;
const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);
if (value && index < 5) {
inputRefs.current[index + 1]?.focus();
}
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>, index: number): void => {
if (e.key === 'Backspace' && !otp[index] && index > 0) {
inputRefs.current[index - 1]?.focus();
}
};
const handlePaste = (e: ClipboardEvent<HTMLInputElement>): void => {
e.preventDefault();
const pastedData = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, 6);
const newOtp = [...otp];
for (let i = 0; i < pastedData.length; i++) {
newOtp[i] = pastedData[i];
}
setOtp(newOtp);
const nextIndex = Math.min(pastedData.length, 5);
inputRefs.current[nextIndex]?.focus();
};
return (
<div className="flex gap-3 items-center justify-center">
{otp.map((digit, index) => (
<motion.input
key={index}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.25,
delay: index * 0.04,
ease: smoothEasing,
}}
ref={(el) => { inputRefs.current[index] = el; }}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
onChange={(e) => handleChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(e, index)}
onPaste={handlePaste}
className="
w-[44px] h-[44px]
bg-[rgba(255,255,255,0.15)]
border border-[rgba(255,255,255,0.1)]
rounded-[8px]
text-center text-white text-[18px] font-medium
outline-none
focus:border-[rgba(255,255,255,0.4)]
focus:bg-[rgba(255,255,255,0.2)]
transition-all duration-200
"
placeholder="•"
aria-label={`Digit ${index + 1}`}
/>
))}
</div>
);
}
/**
* ForgotPasswordModal component
* @description Modal with smooth transitions between email input and OTP verification.
* @param {ForgotPasswordModalProps} props - Component props
* @returns {JSX.Element} ForgotPasswordModal element
*/
export function ForgotPasswordModal({
isOpen,
onClose,
onSubmit,
onVerify,
}: ForgotPasswordModalProps): JSX.Element {
const [step, setStep] = useState<ModalStep>('email');
const [email, setEmail] = useState('');
const [otp, setOtp] = useState<string[]>(['', '', '', '', '', '']);
const [isLoading, setIsLoading] = useState(false);
const handleClose = (): void => {
onClose();
setTimeout(() => {
setStep('email');
setEmail('');
setOtp(['', '', '', '', '', '']);
setIsLoading(false);
}, 250);
};
const handleEmailSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
setIsLoading(true);
onSubmit?.(email);
setTimeout(() => {
setIsLoading(false);
setStep('verify');
}, 600);
};
const handleVerifySubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
const otpString = otp.join('');
if (otpString.length !== 6) return;
setIsLoading(true);
onVerify?.(otpString);
setTimeout(() => {
setIsLoading(false);
handleClose();
}, 800);
};
const handleResendCode = (): void => {
console.log('Resend code to:', email);
};
const handleBackdropClick = (): void => handleClose();
const handleModalClick = (e: React.MouseEvent): void => e.stopPropagation();
const isEmailStep = step === 'email';
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
key="backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25, ease: smoothEasing }}
onClick={handleBackdropClick}
className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm"
/>
{/* Modal Container */}
<motion.div
key="modal-container"
initial={{ opacity: 0, scale: 0.96, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.96, y: 10 }}
transition={{ duration: 0.25, ease: smoothEasing }}
onClick={handleBackdropClick}
className="fixed inset-0 z-50 flex items-center justify-center p-4"
>
{/* Modal Card */}
<motion.div
layout
transition={{ duration: 0.3, ease: smoothEasing }}
onClick={handleModalClick}
className="
flex flex-col items-center gap-8
w-[520px] max-w-full
p-[42px]
rounded-[32px]
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
"
style={{
background: 'linear-gradient(180deg, #00B8B7 0%, #001C8E 100%)',
}}
>
{/* Header with Close Button */}
<div className="flex items-start justify-between w-full gap-4">
{/* Title Section */}
<div className="flex-1 flex flex-col gap-[6px] items-center text-center">
<AnimatePresence mode="wait">
<motion.div
key={isEmailStep ? 'email-header' : 'verify-header'}
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 5 }}
transition={{ duration: 0.2, ease: smoothEasing }}
>
<h2 className="text-[24px] font-semibold leading-[38px] text-white">
{isEmailStep ? 'Forgot your password' : 'Verify Your Email'}
</h2>
<p className={isEmailStep
? "text-[24px] font-semibold leading-[38px] text-white"
: "text-[18px] font-medium leading-6 text-[rgba(255,255,255,0.75)] mt-1"
}>
{isEmailStep ? 'and continue' : 'Enter the 6-Digit Verification Code'}
</p>
</motion.div>
</AnimatePresence>
</div>
{/* Close Button */}
<button
type="button"
onClick={handleClose}
className="
flex items-center justify-center
w-8 h-8 rounded-full
bg-[rgba(255,255,255,0.1)]
hover:bg-[rgba(255,255,255,0.2)]
transition-colors duration-200
cursor-pointer shrink-0
"
aria-label="Close modal"
>
<CloseIcon />
</button>
</div>
{/* Form Content */}
<div className="flex flex-col gap-6 items-center w-full">
<AnimatePresence mode="wait">
{isEmailStep ? (
<motion.form
key="email-form"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.25, ease: smoothEasing }}
onSubmit={handleEmailSubmit}
className="flex flex-col gap-6 w-full"
>
<AuthInput
label="Email"
type="email"
placeholder="Enter your email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
isRequired
autoComplete="email"
/>
<AuthButton isLoading={isLoading}>
Submit
</AuthButton>
</motion.form>
) : (
<motion.form
key="otp-form"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.25, ease: smoothEasing }}
onSubmit={handleVerifySubmit}
className="flex flex-col gap-6 w-full items-center"
>
<OTPInput otp={otp} setOtp={setOtp} />
<AuthButton
isLoading={isLoading}
disabled={otp.join('').length !== 6}
fullWidth
>
Continue
</AuthButton>
</motion.form>
)}
</AnimatePresence>
</div>
{/* Footer */}
<AnimatePresence mode="wait">
{isEmailStep ? (
<motion.button
key="back-btn"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, ease: smoothEasing }}
type="button"
onClick={handleClose}
className="
flex items-center justify-center gap-1.5
text-sm font-semibold text-white
cursor-pointer hover:opacity-80
transition-opacity duration-200
"
>
<ArrowLeftIcon />
<span>Back to Sign In</span>
</motion.button>
) : (
<motion.div
key="resend-code"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, ease: smoothEasing }}
className="flex items-center justify-center gap-1"
>
<span className="text-sm font-normal text-[rgba(255,255,255,0.5)]">
Didn't you receive any code?
</span>
<button
type="button"
onClick={handleResendCode}
className="text-sm font-semibold text-white cursor-pointer hover:underline"
>
Resend Code
</button>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</motion.div>
</>
)}
</AnimatePresence>
);
}

View File

@ -0,0 +1,12 @@
/**
* Auth Components Barrel Export
* @description Exports all authentication-related components.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
export { AuthCard, AuthCardHeader, AuthCardContent } from './auth-card';
export { AuthInput } from './auth-input';
export { AuthButton, SocialButton, TextButton, GoogleIcon, AzureIcon } from './auth-button';
export { AuthFormCard } from './auth-form-card';
export { ForgotPasswordModal } from './forgot-password-modal';

190
src/pages/login-page.tsx Normal file
View File

@ -0,0 +1,190 @@
/**
* Login Page Component
* @description Main authentication page with 2-column responsive layout.
* Left column displays branding/marketing content, right column contains auth form.
* Supports both Sign In and Register modes with animated transitions.
* Includes Forgot Password modal with OTP verification.
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
*/
import { useState } from 'react';
import agenticiqLogo from '@/assets/images/logo/AgenticIQLogo.svg';
import topRightGlow from '@/assets/images/backgrounds/top-right-glow.svg';
import bottomLeftWave1 from '@/assets/images/backgrounds/bottom-left-wave1 .svg';
import bottomLeftWave2 from '@/assets/images/backgrounds/bottom-left-wave2.svg';
import logoGlowBg from '@/assets/images/backgrounds/logo-glow-bg.svg';
import { AuthFormCard, ForgotPasswordModal } from '@/components/auth';
/**
* LoginPage component
* @description Full-page login layout with responsive 2-column grid structure.
* Mobile-first design that stacks columns on smaller screens.
* @returns {JSX.Element} LoginPage element
*/
export function LoginPage(): JSX.Element {
const [showForgotPwd, setShowForgotPwd] = useState(false);
const handleSignIn = (email: string, password: string): void => {
console.log('Sign in:', { email, password });
};
const handleRegister = (data: {
firstName: string;
lastName: string;
email: string;
password: string;
}): void => {
console.log('Register:', data);
};
const handleGoogleAuth = (): void => {
console.log('Google auth clicked');
};
const handleAzureAuth = (): void => {
console.log('Azure auth clicked');
};
const handleForgotPasswordClick = (): void => {
setShowForgotPwd(true);
};
const handleForgotPasswordClose = (): void => {
setShowForgotPwd(false);
};
const handleForgotPasswordSubmit = (email: string): void => {
console.log('Password recovery requested for:', email);
};
const handleOtpVerify = (otp: string): void => {
console.log('OTP verification:', otp);
};
return (
<div className="login-page">
<div className="relative min-h-screen w-full overflow-hidden bg-white">
{/* Decorative Background Elements */}
<BackgroundDecorations />
{/* AgenticIQ Logo */}
<div className="absolute left-[80px] top-[54px] z-20">
<img
src={agenticiqLogo}
alt="AgenticIQ Logo"
className="h-[117px] w-auto"
/>
</div>
{/* Left Side Text Content */}
<LeftColumnText />
{/* Main Content Grid */}
<div className="relative z-10 grid min-h-screen grid-cols-1 lg:grid-cols-[1fr_600px] xl:grid-cols-[1fr_620px]">
{/* Left Column - Branding/Marketing Content */}
<LeftColumn />
{/* Right Column - Auth Form Card */}
<div className="flex items-center justify-start px-6 py-12 lg:px-0 lg:py-16">
<AuthFormCard
initialMode="signin"
onSignIn={handleSignIn}
onRegister={handleRegister}
onGoogleAuth={handleGoogleAuth}
onAzureAuth={handleAzureAuth}
onForgotPassword={handleForgotPasswordClick}
/>
</div>
</div>
</div>
{/* Forgot Password Modal */}
<ForgotPasswordModal
isOpen={showForgotPwd}
onClose={handleForgotPasswordClose}
onSubmit={handleForgotPasswordSubmit}
onVerify={handleOtpVerify}
/>
</div>
);
}
/**
* BackgroundDecorations component
* @description Renders decorative background images matching Figma design.
* @returns {JSX.Element} Background decoration elements
*/
function BackgroundDecorations(): JSX.Element {
return (
<div className="pointer-events-none absolute inset-0 z-0" aria-hidden="true">
{/* Logo glow background */}
<img
src={logoGlowBg}
alt=""
className="absolute left-0 top-0 h-auto w-auto opacity-[0.08]"
style={{ width: '550px', height: 'auto', maxHeight: '350px' }}
/>
{/* Top-right glow decoration */}
<img
src={topRightGlow}
alt=""
className="absolute right-0 top-0 h-auto w-auto opacity-[0.15]"
style={{ width: '515px', height: 'auto', maxHeight: '614px' }}
/>
{/* Bottom-left wave decoration 1 */}
<img
src={bottomLeftWave1}
alt=""
className="absolute bottom-0 left-0 h-auto w-auto opacity-[0.15]"
style={{ width: '559px', height: 'auto', maxHeight: '613px' }}
/>
{/* Bottom-left wave decoration 2 */}
<img
src={bottomLeftWave2}
alt=""
className="absolute bottom-0 left-0 h-auto w-auto opacity-[0.15]"
style={{ width: '350px', height: 'auto', maxHeight: '500px' }}
/>
</div>
);
}
/**
* LeftColumnText component
* @description Left side text content (heading and description).
* @returns {JSX.Element} Text elements positioned as per Figma
*/
function LeftColumnText(): JSX.Element {
return (
<>
<h1
className="absolute text-[36px] font-semibold text-black leading-normal whitespace-pre-wrap z-20"
style={{ left: '110px', top: '348px', width: '513px' }}
>
Engineering the Future with Intelligent Agents
</h1>
<p
className="absolute text-[24px] font-normal text-[rgba(0,0,0,0.75)] leading-normal whitespace-pre-wrap z-20"
style={{ left: '110px', top: '482px', width: '607px' }}
>
Deploy intelligent agents that automate complex workflows, enhance decision-making, and scale your operations. Built for enterprise reliability with seamless integration capabilities.
</p>
</>
);
}
/**
* LeftColumn component
* @description Empty container for left side content.
* @returns {JSX.Element} Left column container
*/
function LeftColumn(): JSX.Element {
return (
<div className="flex flex-col justify-center px-6 py-12 sm:px-12 md:px-16 lg:px-20 xl:px-28">
{/* Text content is positioned absolutely on page level */}
</div>
);
}

View File

@ -1,53 +0,0 @@
/**
* Sign In Page
* @description Basic sign-in form for existing users.
*/
import { Link } from '@tanstack/react-router';
export function SignInPage() {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-[#C8FAFF] via-[#F5F8FF] to-[#C6D7FE] px-4 py-12">
<div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
<div className="mb-6 text-center">
<img src="/Logo.png" alt="AgenticIQ Logo" className="mx-auto h-10 w-auto" />
<h1 className="mt-4 text-2xl font-semibold text-gray-900"> back</h1>
<p className="mt-1 text-sm text-gray-500">Sign in to continue to your dashboard.</p>
</div>
<form className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">Email</label>
<input
type="email"
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
placeholder="you@example.com"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">Password</label>
<input
type="password"
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
placeholder="••••••••"
/>
</div>
<button
type="submit"
className="w-full rounded-lg bg-[#0033FF] px-4 py-2.5 text-sm font-semibold text-white shadow-lg shadow-blue-500/20 transition hover:bg-[#0042CC]"
>
Sign In
</button>
</form>
<p className="mt-4 text-center text-sm text-gray-600">
New here?{' '}
<Link to="/signup" className="font-semibold text-[#0033FF] hover:underline">
Create an account
</Link>
</p>
</div>
</div>
);
}

View File

@ -6,8 +6,8 @@
import { createRootRoute, createRoute, createRouter, Outlet } from '@tanstack/react-router'; import { createRootRoute, createRoute, createRouter, Outlet } from '@tanstack/react-router';
import { RootLayout } from '@/components/layout'; import { RootLayout } from '@/components/layout';
import { Dashboard } from '@/pages/dashboard'; import { Dashboard } from '@/pages/dashboard';
import { SignInPage } from '@/pages/sign-in';
import { SignUpPage } from '@/pages/sign-up'; import { SignUpPage } from '@/pages/sign-up';
import { LoginPage } from '@/pages/login-page';
const rootRoute = createRootRoute({ const rootRoute = createRootRoute({
component: () => <Outlet />, component: () => <Outlet />,
@ -29,10 +29,10 @@ const appRoute = createRoute({
), ),
}); });
const signInRoute = createRoute({ const loginRoute = createRoute({
getParentRoute: () => publicRoute, getParentRoute: () => publicRoute,
path: '/', path: '/',
component: SignInPage, component: LoginPage,
}); });
const signUpRoute = createRoute({ const signUpRoute = createRoute({
@ -48,7 +48,7 @@ const dashboardRoute = createRoute({
}); });
const routeTree = rootRoute.addChildren([ const routeTree = rootRoute.addChildren([
publicRoute.addChildren([signInRoute, signUpRoute]), publicRoute.addChildren([loginRoute, signUpRoute]),
appRoute.addChildren([dashboardRoute]), appRoute.addChildren([dashboardRoute]),
]); ]);

14
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
/// <reference types="vite/client" />
/**
* SVG Module Declaration
* @description Allows importing SVG files as modules in TypeScript.
* Supports both default export (URL string) and ReactComponent export.
*/
declare module '*.svg' {
import React = require('react');
export const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
const src: string;
export default src;
}

View File

@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022", "target": "ES2022",
"useDefineForClassFields": true, "useDefineForClassFields": true,
@ -24,9 +25,7 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
/* Path Aliases */ /* Path Aliases */
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {