Compare commits
No commits in common. "feat/Authentication" and "main" have entirely different histories.
feat/Authe
...
main
49
package-lock.json
generated
49
package-lock.json
generated
@ -15,7 +15,6 @@
|
||||
"@tanstack/router-devtools": "^1.143.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.23.26",
|
||||
"lucide-react": "^0.562.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@ -3013,33 +3012,6 @@
|
||||
"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": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@ -3642,21 +3614,6 @@
|
||||
"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": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@ -4190,12 +4147,6 @@
|
||||
"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": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
"@tanstack/router-devtools": "^1.143.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.23.26",
|
||||
"lucide-react": "^0.562.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
# 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
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
# 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
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
# 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
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 536 B |
@ -1,9 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 463 B |
@ -1,12 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 790 B |
@ -1,9 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 650 B |
@ -1,33 +0,0 @@
|
||||
# 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
|
Before Width: | Height: | Size: 77 KiB |
@ -1,27 +0,0 @@
|
||||
# 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" />
|
||||
```
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
# 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"
|
||||
/>
|
||||
```
|
||||
|
||||
@ -1,220 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
min-h-[44px] h-11 md:h-[52px]
|
||||
px-4 md:px-[18px] py-2.5 md:py-[13px]
|
||||
bg-[#00E2E0]
|
||||
border border-[rgba(255,255,255,0.2)]
|
||||
rounded-lg md:rounded-[12px]
|
||||
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
|
||||
text-sm md:text-base md:text-[16px] font-semibold leading-5 md: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-2 md:gap-3
|
||||
px-3 md:px-4 py-2 md:py-2.5
|
||||
bg-white
|
||||
rounded-lg md:rounded-xl
|
||||
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
|
||||
text-xs md:text-sm font-semibold leading-5 md:leading-6 text-black
|
||||
cursor-pointer
|
||||
hover:bg-gray-50
|
||||
active:bg-gray-100
|
||||
transition-colors duration-200
|
||||
min-h-[44px]
|
||||
${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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,108 +0,0 @@
|
||||
/**
|
||||
* 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-5 md:gap-6 lg:gap-6 xl:gap-8
|
||||
w-full max-w-[520px]
|
||||
rounded-2xl md:rounded-3xl lg:rounded-2xl xl:rounded-[32px]
|
||||
shadow-[0px_4px_24px_0px_rgba(0,0,0,0.15)]
|
||||
${isRegister ? 'px-6 py-8 md:px-8 md:py-12 lg:px-8 lg:py-10 xl:px-[42px] xl:py-[64px]' : 'px-6 py-8 md:px-10 md:py-12 lg:px-10 lg:py-12 xl:px-[60px] xl: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-2xl md:text-2xl lg:text-2xl xl:text-3xl 2xl:text-[30px] font-semibold leading-tight md:leading-tight lg:leading-tight xl:leading-[38px] text-white w-full">
|
||||
{title}
|
||||
</h2>
|
||||
{subtitle && (
|
||||
<p className="text-sm md:text-base lg:text-sm xl:text-lg 2xl:text-[18px] font-medium leading-5 md:leading-5 lg:leading-5 xl: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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,333 +0,0 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/** Animation timing constants (in milliseconds) */
|
||||
const ANIMATION_TIMING = {
|
||||
/** Duration for simulating form submission */
|
||||
FORM_SUBMIT_DELAY: 1000,
|
||||
/** Duration for name fields expand/collapse */
|
||||
NAME_FIELDS_DURATION: 0.2,
|
||||
/** Duration for forgot password link animation */
|
||||
FORGOT_PASSWORD_DURATION: 0.15,
|
||||
/** Opacity fade delay */
|
||||
OPACITY_DELAY: 0.05,
|
||||
/** Opacity fade duration */
|
||||
OPACITY_DURATION: 0.15,
|
||||
/** Margin animation bottom value */
|
||||
NAME_FIELDS_MARGIN: 16,
|
||||
/** Margin animation top value */
|
||||
FORGOT_PASSWORD_MARGIN: 8,
|
||||
/** Layout animation duration */
|
||||
LAYOUT_DURATION: 0.3,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 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: ANIMATION_TIMING.NAME_FIELDS_DURATION },
|
||||
opacity: { duration: 0.1 },
|
||||
marginBottom: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
|
||||
},
|
||||
},
|
||||
visible: {
|
||||
height: 'auto',
|
||||
opacity: 1,
|
||||
marginBottom: ANIMATION_TIMING.NAME_FIELDS_MARGIN,
|
||||
transition: {
|
||||
height: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
|
||||
opacity: { duration: ANIMATION_TIMING.OPACITY_DURATION, delay: ANIMATION_TIMING.OPACITY_DELAY },
|
||||
marginBottom: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Animation variants for forgot password link
|
||||
*/
|
||||
const forgotPasswordVariants = {
|
||||
hidden: {
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
marginTop: 0,
|
||||
transition: {
|
||||
height: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
|
||||
opacity: { duration: 0.1 },
|
||||
marginTop: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
|
||||
},
|
||||
},
|
||||
visible: {
|
||||
height: 'auto',
|
||||
opacity: 1,
|
||||
marginTop: ANIMATION_TIMING.FORGOT_PASSWORD_MARGIN,
|
||||
transition: {
|
||||
height: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
|
||||
opacity: { duration: ANIMATION_TIMING.OPACITY_DURATION, delay: ANIMATION_TIMING.OPACITY_DELAY },
|
||||
marginTop: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* AuthFormCard component
|
||||
* @description Unified authentication card with animated transitions between Sign In and Register modes.
|
||||
* Features smooth expand/collapse animations for form fields using framer-motion.
|
||||
* @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>} event - Form submit event
|
||||
*/
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
|
||||
event.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
if (isRegister) {
|
||||
onRegister?.({ firstName, lastName, email, password });
|
||||
} else {
|
||||
onSignIn?.(email, password);
|
||||
}
|
||||
|
||||
setTimeout(() => setIsLoading(false), ANIMATION_TIMING.FORM_SUBMIT_DELAY);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle between Sign In and Register modes
|
||||
* Clears all form fields when switching modes
|
||||
*/
|
||||
const toggleMode = (): void => {
|
||||
setMode((prev) => (prev === 'signin' ? 'register' : 'signin'));
|
||||
setFirstName('');
|
||||
setLastName('');
|
||||
setEmail('');
|
||||
setPassword('');
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
transition={{ layout: { ...springTransition, duration: ANIMATION_TIMING.LAYOUT_DURATION } }}
|
||||
className="w-full max-w-[460px] lg:max-w-[440px] xl:max-w-[520px]"
|
||||
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-3 md:gap-4 lg:gap-3 xl: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>
|
||||
);
|
||||
}
|
||||
@ -1,168 +0,0 @@
|
||||
/**
|
||||
* 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-sm md:text-base lg:text-sm xl:text-base font-medium leading-5 md:leading-6 lg:leading-5 xl: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="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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@ -1,319 +0,0 @@
|
||||
/**
|
||||
* 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, type FormEvent } from 'react';
|
||||
import { motion, AnimatePresence, type Easing } from 'framer-motion';
|
||||
import { AuthInput, AuthButton } from './index';
|
||||
import { OTPInput } from './otp-input';
|
||||
import { CloseIcon, ArrowLeftIcon } from './modal-icons';
|
||||
|
||||
/** Animation timing constants (in milliseconds) */
|
||||
const ANIMATION_TIMING = {
|
||||
MODAL_RESET_DELAY: 250,
|
||||
EMAIL_SUBMIT_DELAY: 600,
|
||||
OTP_VERIFY_DELAY: 800,
|
||||
TRANSITION_DURATION: 0.25,
|
||||
LAYOUT_DURATION: 0.3,
|
||||
HEADER_DURATION: 0.2,
|
||||
OTP_LENGTH: 6,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Smooth easing curve for animations (cubic-bezier)
|
||||
* Material Design standard easing
|
||||
*/
|
||||
const smoothEasing: Easing = [0.4, 0, 0.2, 1];
|
||||
|
||||
/**
|
||||
* ForgotPasswordModal component
|
||||
* @description Modal with smooth transitions between email input and OTP verification.
|
||||
* Features animated backdrop, scale/fade modal entrance, and sliding form content.
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* Handle modal close and reset state
|
||||
*/
|
||||
const handleClose = (): void => {
|
||||
onClose();
|
||||
setTimeout(() => {
|
||||
setStep('email');
|
||||
setEmail('');
|
||||
setOtp(['', '', '', '', '', '']);
|
||||
setIsLoading(false);
|
||||
}, ANIMATION_TIMING.MODAL_RESET_DELAY);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle email form submission
|
||||
* @param {FormEvent<HTMLFormElement>} event - Form submit event
|
||||
*/
|
||||
const handleEmailSubmit = (event: FormEvent<HTMLFormElement>): void => {
|
||||
event.preventDefault();
|
||||
setIsLoading(true);
|
||||
onSubmit?.(email);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setStep('verify');
|
||||
}, ANIMATION_TIMING.EMAIL_SUBMIT_DELAY);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle OTP verification form submission
|
||||
* @param {FormEvent<HTMLFormElement>} event - Form submit event
|
||||
*/
|
||||
const handleVerifySubmit = (event: FormEvent<HTMLFormElement>): void => {
|
||||
event.preventDefault();
|
||||
const otpString = otp.join('');
|
||||
if (otpString.length !== ANIMATION_TIMING.OTP_LENGTH) return;
|
||||
|
||||
setIsLoading(true);
|
||||
onVerify?.(otpString);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
handleClose();
|
||||
}, ANIMATION_TIMING.OTP_VERIFY_DELAY);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle resend code request
|
||||
*/
|
||||
const handleResendCode = (): void => {
|
||||
// TODO(AUTH-007): Implement resend OTP API call
|
||||
onSubmit?.(email);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle backdrop click to close modal
|
||||
*/
|
||||
const handleBackdropClick = (): void => handleClose();
|
||||
|
||||
/**
|
||||
* Prevent modal card clicks from closing modal
|
||||
* @param {React.MouseEvent} event - Mouse event
|
||||
*/
|
||||
const handleModalClick = (event: React.MouseEvent): void => event.stopPropagation();
|
||||
|
||||
const isEmailStep = step === 'email';
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
key="backdrop"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: ANIMATION_TIMING.TRANSITION_DURATION, 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: ANIMATION_TIMING.TRANSITION_DURATION, ease: smoothEasing }}
|
||||
onClick={handleBackdropClick}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
>
|
||||
{/* Modal Card */}
|
||||
<motion.div
|
||||
layout
|
||||
transition={{ duration: ANIMATION_TIMING.LAYOUT_DURATION, ease: smoothEasing }}
|
||||
onClick={handleModalClick}
|
||||
className="
|
||||
flex flex-col items-center gap-6 md:gap-7 lg:gap-8
|
||||
w-full max-w-[520px]
|
||||
p-6 md:p-8 lg:p-[42px]
|
||||
rounded-2xl md:rounded-3xl lg:rounded-[32px]
|
||||
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
|
||||
mx-4
|
||||
"
|
||||
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: ANIMATION_TIMING.HEADER_DURATION, ease: smoothEasing }}
|
||||
>
|
||||
<h2 className="text-xl md:text-xl lg:text-2xl xl:text-[24px] font-semibold leading-tight md:leading-tight lg:leading-[38px] text-white">
|
||||
{isEmailStep ? 'Forgot your password' : 'Verify Your Email'}
|
||||
</h2>
|
||||
<p className={isEmailStep
|
||||
? "text-xl md:text-xl lg:text-2xl xl:text-[24px] font-semibold leading-tight md:leading-tight lg:leading-[38px] text-white"
|
||||
: "text-base md:text-base lg:text-lg xl:text-[18px] font-medium leading-5 md:leading-5 lg: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 md:w-10 md:h-10 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 min-w-[44px] min-h-[44px]
|
||||
"
|
||||
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: ANIMATION_TIMING.TRANSITION_DURATION, 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: ANIMATION_TIMING.TRANSITION_DURATION, 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 !== ANIMATION_TIMING.OTP_LENGTH}
|
||||
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: ANIMATION_TIMING.HEADER_DURATION, 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
|
||||
min-h-[44px]
|
||||
"
|
||||
>
|
||||
<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: ANIMATION_TIMING.HEADER_DURATION, ease: smoothEasing }}
|
||||
className="flex flex-col sm:flex-row items-center justify-center gap-1 text-center sm:text-left"
|
||||
>
|
||||
<span className="text-xs sm: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-xs sm:text-sm font-semibold text-white cursor-pointer hover:underline min-h-[44px]"
|
||||
>
|
||||
Resend Code
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* 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';
|
||||
export { OTPInput } from './otp-input';
|
||||
export { CloseIcon, ArrowLeftIcon } from './modal-icons';
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
/**
|
||||
* Modal Icons Component
|
||||
* @description Icon components for modal dialogs
|
||||
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Close icon component
|
||||
* @description Renders an X icon for closing the modal
|
||||
* @returns {JSX.Element} Close icon SVG
|
||||
*/
|
||||
export 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
|
||||
* @description Renders a left-pointing arrow for back navigation
|
||||
* @returns {JSX.Element} Arrow left icon SVG
|
||||
*/
|
||||
export 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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,139 +0,0 @@
|
||||
/**
|
||||
* OTP Input Component
|
||||
* @description Six individual input boxes for OTP entry with auto-focus,
|
||||
* backspace navigation, and paste support.
|
||||
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
|
||||
*/
|
||||
|
||||
import { useRef, useEffect, type KeyboardEvent, type ClipboardEvent } from 'react';
|
||||
import { motion, type Easing } from 'framer-motion';
|
||||
|
||||
/** Animation timing constants */
|
||||
const ANIMATION_TIMING = {
|
||||
OTP_AUTOFOCUS_DELAY: 300,
|
||||
TRANSITION_DURATION: 0.25,
|
||||
OTP_STAGGER_DELAY: 0.04,
|
||||
OTP_LENGTH: 6,
|
||||
OTP_MAX_INDEX: 5,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Props for OTPInput component
|
||||
*/
|
||||
interface OTPInputProps {
|
||||
/** Current OTP values array */
|
||||
otp: string[];
|
||||
/** Setter function for OTP state */
|
||||
setOtp: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Smooth easing curve for animations
|
||||
*/
|
||||
const smoothEasing: Easing = [0.4, 0, 0.2, 1];
|
||||
|
||||
/**
|
||||
* OTP Input component
|
||||
* @description Six individual input boxes for OTP entry with auto-focus,
|
||||
* backspace navigation, and paste support.
|
||||
* @param {OTPInputProps} props - Component props
|
||||
* @returns {JSX.Element} OTP input element
|
||||
*/
|
||||
export function OTPInput({ otp, setOtp }: OTPInputProps): JSX.Element {
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
inputRefs.current[0]?.focus();
|
||||
}, ANIMATION_TIMING.OTP_AUTOFOCUS_DELAY);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle input value change
|
||||
* @param {number} index - Input index
|
||||
* @param {string} value - New input value
|
||||
*/
|
||||
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 < ANIMATION_TIMING.OTP_MAX_INDEX) {
|
||||
inputRefs.current[index + 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle keyboard navigation
|
||||
* @param {KeyboardEvent<HTMLInputElement>} event - Keyboard event
|
||||
* @param {number} index - Current input index
|
||||
*/
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>, index: number): void => {
|
||||
if (event.key === 'Backspace' && !otp[index] && index > 0) {
|
||||
inputRefs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle paste event for OTP auto-fill
|
||||
* @param {ClipboardEvent<HTMLInputElement>} event - Clipboard event
|
||||
*/
|
||||
const handlePaste = (event: ClipboardEvent<HTMLInputElement>): void => {
|
||||
event.preventDefault();
|
||||
const pastedData = event.clipboardData
|
||||
.getData('text')
|
||||
.replace(/\D/g, '')
|
||||
.slice(0, ANIMATION_TIMING.OTP_LENGTH);
|
||||
const newOtp = [...otp];
|
||||
for (let i = 0; i < pastedData.length; i++) {
|
||||
newOtp[i] = pastedData[i];
|
||||
}
|
||||
setOtp(newOtp);
|
||||
const nextIndex = Math.min(pastedData.length, ANIMATION_TIMING.OTP_MAX_INDEX);
|
||||
inputRefs.current[nextIndex]?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 md:gap-3 items-center justify-center w-full">
|
||||
{otp.map((digit, index) => (
|
||||
<motion.input
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: ANIMATION_TIMING.TRANSITION_DURATION,
|
||||
delay: index * ANIMATION_TIMING.OTP_STAGGER_DELAY,
|
||||
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-10 h-10 md:w-[44px] md:h-[44px]
|
||||
bg-[rgba(255,255,255,0.15)]
|
||||
border border-[rgba(255,255,255,0.1)]
|
||||
rounded-lg md:rounded-[8px]
|
||||
text-center text-white text-base md: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
|
||||
flex-1 max-w-[44px]
|
||||
"
|
||||
placeholder="•"
|
||||
aria-label={`Digit ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -27,15 +27,15 @@ export function AgentCard({ title, tags, description }: AgentCardProps) {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg sm:rounded-xl p-4 sm:p-6 shadow-sm border border-gray-100 card-hover">
|
||||
<h3 className="text-base sm:text-lg font-semibold text-gray-900 mb-3">{title}</h3>
|
||||
<div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100 card-hover">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3">{title}</h3>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
/* Use the index to pick the color; fall back to the first color if index > 2 */
|
||||
className={`${tagColors[index] || tagColors[0]} px-2 sm:px-3 py-1 text-xs font-medium text-gray-700 border border-gray-200 rounded`}
|
||||
className={`${tagColors[index] || tagColors[0]} px-3 py-1 text-xs font-medium text-gray-700 border border-gray-200`}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
|
||||
@ -51,17 +51,17 @@ export function MetricCard({ title, subtitle, value, gradient }: MetricCardProps
|
||||
// </div>
|
||||
// );
|
||||
return (
|
||||
<div className={`p-4 sm:p-6 h-auto min-h-[100px] sm:h-[110px] rounded-xl sm:rounded-2xl ${gradients[gradient]} w-full`}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className={`p-6 h-[110px] rounded-2xl ${gradients[gradient]} w-full `}>
|
||||
<div className="flex items-center justify-between w-full ">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] sm:text-[12px] font-medium text-white uppercase tracking-wider">
|
||||
<span className="text-[12px] font-medium text-white uppercase tracking-wider">
|
||||
{title}
|
||||
</span>
|
||||
<span className="text-base sm:text-lg md:text-[18px] font-bold mt-1 text-white">
|
||||
<span className="text-[18px] font-bold mt-1 text-white">
|
||||
{subtitle}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-3xl sm:text-4xl md:text-5xl font-bold text-white">
|
||||
<div className="text-5xl font-bold text-white">
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -12,47 +12,47 @@ import { Home, Settings } from 'lucide-react';
|
||||
*/
|
||||
export function Sidebar() {
|
||||
return (
|
||||
<aside className="fixed left-2 sm:left-4 md:left-6 top-20 sm:top-24 md:top-24 bottom-2 sm:bottom-4 md:bottom-6 w-12 sm:w-14 md:w-16 bg-gradient-to-b from-[#00A1A0] to-[#00166D] flex flex-col items-center py-4 sm:py-5 md:py-6 gap-4 sm:gap-5 md:gap-6 z-50 rounded-xl sm:rounded-2xl shadow-xl hidden md:flex">
|
||||
<aside className="fixed left-6 top-24 bottom-6 w-16 bg-gradient-to-b from-[#00A1A0] to-[#00166D] flex flex-col items-center py-6 gap-6 z-50 rounded-2xl shadow-xl">
|
||||
{/* Navigation Icons */}
|
||||
<nav className="flex flex-col items-center gap-3 sm:gap-4 md:gap-5 flex-1 w-full">
|
||||
<nav className="flex flex-col items-center gap-5 flex-1 w-full">
|
||||
{/* Home - Active State */}
|
||||
<button className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px] bg-white/20 rounded-lg sm:rounded-xl flex items-center justify-center text-white shadow-inner transition-all hover:bg-white/30 min-w-[44px] min-h-[44px]" aria-label="Home">
|
||||
<Home className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||
<button className="w-[42px] h-[42px] bg-white/20 rounded-xl flex items-center justify-center text-white shadow-inner transition-all hover:bg-white/30" aria-label="Home">
|
||||
<Home className="w-5 h-5 text-white" />
|
||||
</button>
|
||||
|
||||
<button className="sidebar-icon-v3 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Agent">
|
||||
<img src="/Agent.svg" alt="Agent" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||
<button className="sidebar-icon-v3" aria-label="Agent">
|
||||
<img src="/Agent.svg" alt="Agent" className="w-[42px] h-[42px] " />
|
||||
</button>
|
||||
<button className="sidebar-icon-v3 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="KB">
|
||||
<img src="/KB.svg" alt="KB" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||
<button className="sidebar-icon-v3" aria-label="KB">
|
||||
<img src="/KB.svg" alt="KB" className="w-[42px] h-[42px] " />
|
||||
</button>
|
||||
<button className="sidebar-icon-v3 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Models">
|
||||
<img src="/Models.svg" alt="Models" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||
<button className="sidebar-icon-v3" aria-label="Models">
|
||||
<img src="/Models.svg" alt="Models" className="w-[42px] h-[42px] " />
|
||||
</button>
|
||||
<button className="sidebar-icon-v3 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Workflow">
|
||||
<img src="/Workflow.svg" alt="Workflow" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||
<button className="sidebar-icon-v3" aria-label="Workflow">
|
||||
<img src="/Workflow.svg" alt="Workflow" className="w-[42px] h-[42px] " />
|
||||
</button>
|
||||
<button className="sidebar-icon-v3 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Nav Item">
|
||||
<img src="/Nav item.svg" alt="Nav Item" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||
<button className="sidebar-icon-v3" aria-label="Nav Item">
|
||||
<img src="/Nav item.svg" alt="Nav Item" className="w-[42px] h-[42px] " />
|
||||
</button>
|
||||
<button className="sidebar-icon-v3 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="LLM">
|
||||
<img src="/LLM.svg" alt="LLM" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||
<button className="sidebar-icon-v3" aria-label="LLM">
|
||||
<img src="/LLM.svg" alt="LLM" className="w-[42px] h-[42px] " />
|
||||
</button>
|
||||
<button className="sidebar-icon-v3 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="FT">
|
||||
<img src="/FT.svg" alt="FT" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||
<button className="sidebar-icon-v3" aria-label="FT">
|
||||
<img src="/FT.svg" alt="FT" className="w-[42px] h-[42px] " />
|
||||
</button>
|
||||
<button className="sidebar-icon-v3 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Task Watcher">
|
||||
<img src="/Task watacher.svg" alt="Task Watcher" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||
<button className="sidebar-icon-v3" aria-label="Task Watcher">
|
||||
<img src="/Task watacher.svg" alt="Task Watcher" className="w-[42px] h-[42px] " />
|
||||
</button>
|
||||
<button className="sidebar-icon-v3 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Optimization">
|
||||
<img src="/Optimization.svg" alt="Optimization" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||
<button className="sidebar-icon-v3" aria-label="Optimization">
|
||||
<img src="/Optimization.svg" alt="Optimization" className="w-[42px] h-[42px] " />
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* Bottom Icon */}
|
||||
<div className="flex flex-col gap-3 sm:gap-4 mt-auto">
|
||||
<button className="sidebar-icon-v3 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Settings">
|
||||
<Settings className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||
<div className="flex flex-col gap-4 mt-auto">
|
||||
<button className="sidebar-icon-v3" aria-label="Settings">
|
||||
<Settings className="w-5 h-5 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@ -15,7 +15,7 @@ export function Header() {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 mt-4 sm:mt-6 md:mt-6 lg:mt-[24px] mb-2 sm:mb-4 md:mb-4 lg:mb-[16px] left-0 right-0 h-14 sm:h-16 md:h-16 z-[100] backdrop-blur-md flex items-center justify-between px-4 sm:px-6 md:px-6 lg:px-8">
|
||||
<header className="fixed top-0 mt-[24px] mb-[16px] left-0 right-0 h-16 z-[100] backdrop-blur-md flex items-center justify-between px-8">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
@ -23,51 +23,51 @@ export function Header() {
|
||||
alt="AgenticIQ Logo"
|
||||
width={168}
|
||||
height={60}
|
||||
className="object-contain h-8 sm:h-10 md:h-auto w-auto"
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Actions */}
|
||||
<div className="bg-white rounded-xl sm:rounded-[20px] p-1.5 sm:p-2 flex items-center gap-1 sm:gap-2 shadow-sm border border-gray-100/50">
|
||||
<div className="bg-white rounded-[20px] p-2 flex items-center gap-2 shadow-sm border border-gray-100/50">
|
||||
{/* Bell Notification */}
|
||||
<button className="p-2 sm:p-2.5 bg-[#F4F9FF] hover:bg-[#E9F3FF] rounded-lg sm:rounded-xl transition-all duration-200 min-w-[44px] min-h-[44px] flex items-center justify-center">
|
||||
<Bell className="w-4 h-4 sm:w-5 sm:h-5 text-[#1A1A1A]" strokeWidth={1.5} />
|
||||
<button className="p-2.5 bg-[#F4F9FF] hover:bg-[#E9F3FF] rounded-xl transition-all duration-200">
|
||||
<Bell className="w-5 h-5 text-[#1A1A1A]" strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
{/* Theme Toggles */}
|
||||
<div className="flex items-center gap-1 sm:gap-1.5 px-0.5 sm:px-1">
|
||||
<div className="flex items-center gap-1.5 px-1">
|
||||
<button
|
||||
onClick={() => setTheme('light')}
|
||||
className={`p-2 sm:p-2.5 rounded-full transition-all duration-300 min-w-[44px] min-h-[44px] flex items-center justify-center ${
|
||||
className={`p-2.5 rounded-full transition-all duration-300 ${
|
||||
theme === 'light'
|
||||
? 'bg-[#0033FF] text-white shadow-lg shadow-blue-500/40 scale-105'
|
||||
: 'bg-[#F4F9FF] text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<Sun className="w-4 h-4 sm:w-5 sm:h-5" strokeWidth={theme === 'light' ? 2 : 1.5} />
|
||||
<Sun className="w-5 h-5" strokeWidth={theme === 'light' ? 2 : 1.5} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme('dark')}
|
||||
className={`p-2 sm:p-2.5 rounded-full transition-all duration-300 min-w-[44px] min-h-[44px] flex items-center justify-center ${
|
||||
className={`p-2.5 rounded-full transition-all duration-300 ${
|
||||
theme === 'dark'
|
||||
? 'bg-[#0052FF] text-white shadow-lg shadow-blue-500/40 scale-105'
|
||||
: 'bg-[#F4F9FF] text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<Moon className="w-4 h-4 sm:w-5 sm:h-5" strokeWidth={theme === 'dark' ? 2 : 1.5} />
|
||||
<Moon className="w-5 h-5" strokeWidth={theme === 'dark' ? 2 : 1.5} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Profile Section */}
|
||||
<div className="flex items-center gap-1 sm:gap-2 pl-0.5 sm:pl-1 pr-1 sm:pr-2">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-full overflow-hidden border-2 border-white shadow-sm ring-1 ring-gray-100">
|
||||
<div className="flex items-center gap-2 pl-1 pr-2">
|
||||
<div className="w-10 h-10 rounded-full overflow-hidden border-2 border-white shadow-sm ring-1 ring-gray-100">
|
||||
<img
|
||||
src="/profile.svg"
|
||||
alt="User Avatar"
|
||||
className="w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
<ChevronDown className="w-3 h-3 sm:w-4 sm:h-4 text-gray-900 hidden sm:block" strokeWidth={2.5} />
|
||||
<ChevronDown className="w-4 h-4 text-gray-900" strokeWidth={2.5} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -38,7 +38,7 @@ export function RootLayout({ children }: RootLayoutProps) {
|
||||
<Sidebar />
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="md:ml-20 lg:ml-24 pt-14 sm:pt-16 md:pt-16 lg:pt-16 min-h-screen">
|
||||
<main className="ml-24 pt-16 min-h-screen">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -51,38 +51,37 @@ export function Dashboard() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6 md:pt-8">
|
||||
<div className="px-8 pt-8">
|
||||
{/* Content Header: Welcome + Search/Create */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6 md:mb-8">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-[22px] font-semibold text-gray-900 mb-2">
|
||||
<h1 className="text-[22px] font-semibold text-gray-900 mb-2">
|
||||
Welcome, Vijay! 👋
|
||||
</h1>
|
||||
<p className="text-gray-500 max-w-2xl text-sm sm:text-base md:text-[16px]">
|
||||
<p className="text-gray-500 max-w-2xl text-[16px]">
|
||||
Monitor and manage your intelligent agents, knowledge bases, and automated workflows from a centralized dashboard
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 w-full sm:w-auto">
|
||||
<div className="relative group flex-1 sm:flex-initial">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative group">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 group-focus-within:text-blue-500 transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
className="pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 w-full sm:w-64 shadow-sm transition-all min-h-[44px]"
|
||||
className="pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 w-64 shadow-sm transition-all"
|
||||
/>
|
||||
</div>
|
||||
<Button className="bg-[#0033FF] hover:bg-[#0042CC] text-white px-4 sm:px-6 py-2.5 rounded-xl font-semibold flex items-center justify-center gap-2 shadow-lg shadow-blue-500/20 active:scale-95 transition-all h-auto min-h-[44px] w-full sm:w-auto">
|
||||
<Button className="bg-[#0033FF] hover:bg-[#0042CC] text-white px-6 py-2.5 rounded-xl font-semibold flex items-center gap-2 shadow-lg shadow-blue-500/20 active:scale-95 transition-all h-auto">
|
||||
<Plus className="w-5 h-5" />
|
||||
<span className="hidden sm:inline">Create Agent</span>
|
||||
<span className="sm:hidden">Create</span>
|
||||
Create Agent
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<div className="bg-[#F3F8FF]/80 backdrop-blur-sm rounded-2xl md:rounded-[32px] p-4 sm:p-6 md:p-6 mb-6 md:mb-8 border border-white/40 shadow-sm">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="bg-[#F3F8FF]/80 backdrop-blur-sm rounded-[32px] p-6 mb-8 border border-white/40 shadow-sm">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<MetricCard
|
||||
title="ACTIVE AGENTS"
|
||||
subtitle="Agents"
|
||||
@ -111,12 +110,12 @@ export function Dashboard() {
|
||||
</div>
|
||||
|
||||
{/* Agents Section */}
|
||||
<div className="bg-white rounded-xl md:rounded-2xl shadow-sm border border-gray-200 p-4 sm:p-6">
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 sm:gap-4 mb-4 sm:mb-6 overflow-x-auto">
|
||||
<div className="flex gap-4 mb-6">
|
||||
<button
|
||||
onClick={() => setActiveTab('all')}
|
||||
className={`px-4 sm:px-6 py-2 rounded-lg text-sm sm:text-base font-medium transition-colors whitespace-nowrap min-h-[44px] ${activeTab === 'all'
|
||||
className={`px-6 py-2 rounded-lg text font-medium transition-colors ${activeTab === 'all'
|
||||
? 'bg-black text-white'
|
||||
: 'bg-white text-gray-600 border border-gray-300 hover:bg-gray-50'
|
||||
}`}
|
||||
@ -125,7 +124,7 @@ export function Dashboard() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('my')}
|
||||
className={`px-4 sm:px-6 py-2 rounded-lg text-sm sm:text-base font-medium transition-colors whitespace-nowrap min-h-[44px] ${activeTab === 'my'
|
||||
className={`px-6 py-2 rounded-lg font-medium transition-colors ${activeTab === 'my'
|
||||
? 'bg-black text-white'
|
||||
: 'bg-white text-gray-600 border border-gray-300 hover:bg-gray-50'
|
||||
}`}
|
||||
@ -135,7 +134,7 @@ export function Dashboard() {
|
||||
</div>
|
||||
|
||||
{/* Agent Cards Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-4 md:gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{agents.map((agent, index) => (
|
||||
<AgentCard
|
||||
key={index}
|
||||
|
||||
@ -1,229 +0,0 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* Register form data interface
|
||||
*/
|
||||
interface RegisterFormData {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Handle sign in form submission
|
||||
* @param {string} email - User email address
|
||||
* @param {string} password - User password
|
||||
*/
|
||||
const handleSignIn = (email: string, password: string): void => {
|
||||
// TODO(AUTH-001): Implement actual sign in API call
|
||||
void Promise.resolve({ email, password });
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle register form submission
|
||||
* @param {RegisterFormData} data - Registration form data
|
||||
*/
|
||||
const handleRegister = (data: RegisterFormData): void => {
|
||||
// TODO(AUTH-002): Implement actual registration API call
|
||||
void Promise.resolve(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle Google OAuth authentication
|
||||
*/
|
||||
const handleGoogleAuth = (): void => {
|
||||
// TODO(AUTH-003): Implement Google OAuth flow
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle Azure OAuth authentication
|
||||
*/
|
||||
const handleAzureAuth = (): void => {
|
||||
// TODO(AUTH-004): Implement Azure OAuth flow
|
||||
};
|
||||
|
||||
/**
|
||||
* Open forgot password modal
|
||||
*/
|
||||
const handleForgotPasswordClick = (): void => {
|
||||
setShowForgotPwd(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Close forgot password modal
|
||||
*/
|
||||
const handleForgotPasswordClose = (): void => {
|
||||
setShowForgotPwd(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle forgot password email submission
|
||||
* @param {string} email - Email for password recovery
|
||||
*/
|
||||
const handleForgotPasswordSubmit = (email: string): void => {
|
||||
// TODO(AUTH-005): Implement password recovery API call
|
||||
void Promise.resolve(email);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle OTP verification submission
|
||||
* @param {string} otp - 6-digit OTP code
|
||||
*/
|
||||
const handleOtpVerify = (otp: string): void => {
|
||||
// TODO(AUTH-006): Implement OTP verification API call
|
||||
void Promise.resolve(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-4 top-4 md:left-8 lg:left-12 xl:left-[80px] md:top-8 lg:top-8 xl:top-[54px] z-20">
|
||||
<img
|
||||
src={agenticiqLogo}
|
||||
alt="AgenticIQ Logo"
|
||||
className="h-16 w-auto md:h-24 lg:h-20 xl:h-[117px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Left Side Text Content */}
|
||||
<LeftColumnText />
|
||||
|
||||
{/* Main Content Grid */}
|
||||
<div className="relative z-10 grid min-h-screen grid-cols-1 md:grid-cols-1 lg:grid-cols-[1fr_480px] xl:grid-cols-[1fr_600px] 2xl:grid-cols-[1fr_620px]">
|
||||
{/* Left Column - Branding/Marketing Content */}
|
||||
<LeftColumn />
|
||||
|
||||
{/* Right Column - Auth Form Card */}
|
||||
<div className="flex items-center justify-center md:justify-center lg:justify-start px-4 py-8 md:px-6 md:py-12 lg:px-4 lg:py-12 xl:px-0 xl: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.
|
||||
* All images are purely decorative and hidden from screen readers.
|
||||
* @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] hidden md:block"
|
||||
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] hidden md:block"
|
||||
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] hidden md:block"
|
||||
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] hidden md:block"
|
||||
style={{ width: '350px', height: 'auto', maxHeight: '500px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* LeftColumnText component
|
||||
* @description Left side text content with heading and description.
|
||||
* Positioned absolutely to match Figma design specifications.
|
||||
* @returns {JSX.Element} Text elements positioned as per Figma
|
||||
*/
|
||||
function LeftColumnText(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1
|
||||
className="hidden lg:block absolute text-2xl md:text-3xl lg:text-2xl xl:text-[36px] font-semibold text-black leading-tight lg:leading-tight xl:leading-normal whitespace-pre-wrap z-20 lg:left-12 lg:top-64 lg:w-80 xl:left-[110px] xl:top-[348px] xl:w-[513px]"
|
||||
>
|
||||
Engineering the Future with Intelligent Agents
|
||||
</h1>
|
||||
<p
|
||||
className="hidden lg:block absolute text-sm md:text-base lg:text-sm xl:text-[24px] font-normal text-[rgba(0,0,0,0.75)] leading-relaxed lg:leading-relaxed xl:leading-normal whitespace-pre-wrap z-20 lg:left-12 lg:top-96 lg:w-80 xl:left-[110px] xl:top-[482px] xl:w-[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 in the grid layout.
|
||||
* Text content is positioned absolutely on the page level for precise control.
|
||||
* @returns {JSX.Element} Left column container
|
||||
*/
|
||||
function LeftColumn(): JSX.Element {
|
||||
return (
|
||||
<div className="hidden md:hidden lg:flex flex-col justify-center px-6 py-12 md:px-12 lg:px-8 xl:px-20 2xl:px-28">
|
||||
{/* Text content is positioned absolutely on page level */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
53
src/pages/sign-in.tsx
Normal file
53
src/pages/sign-in.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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">Welcome 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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,12 +7,12 @@ import { Link } from '@tanstack/react-router';
|
||||
|
||||
export function SignUpPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-[#C8FAFF] via-[#F5F8FF] to-[#C6D7FE] px-4 py-8 sm:py-12">
|
||||
<div className="w-full max-w-md rounded-xl md:rounded-2xl bg-white p-6 sm:p-8 shadow-xl">
|
||||
<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-8 w-auto sm:h-10" />
|
||||
<h1 className="mt-4 text-xl sm:text-2xl font-semibold text-gray-900">Create your account</h1>
|
||||
<p className="mt-1 text-xs sm:text-sm text-gray-500">Join AgenticIQ to manage your agents and workflows.</p>
|
||||
<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">Create your account</h1>
|
||||
<p className="mt-1 text-sm text-gray-500">Join AgenticIQ to manage your agents and workflows.</p>
|
||||
</div>
|
||||
|
||||
<form className="space-y-4">
|
||||
@ -20,7 +20,7 @@ export function SignUpPage() {
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">Full name</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2.5 sm:py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 min-h-[44px]"
|
||||
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="Jane Doe"
|
||||
/>
|
||||
</div>
|
||||
@ -28,7 +28,7 @@ export function SignUpPage() {
|
||||
<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.5 sm:py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 min-h-[44px]"
|
||||
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>
|
||||
@ -36,19 +36,19 @@ export function SignUpPage() {
|
||||
<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.5 sm:py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 min-h-[44px]"
|
||||
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-3 sm:py-2.5 text-sm font-semibold text-white shadow-lg shadow-blue-500/20 transition hover:bg-[#0042CC] min-h-[44px]"
|
||||
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 Up
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-4 text-center text-xs sm:text-sm text-gray-600">
|
||||
<p className="mt-4 text-center text-sm text-gray-600">
|
||||
Already have an account?{' '}
|
||||
<Link to="/" className="font-semibold text-[#0033FF] hover:underline">
|
||||
Sign in
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
import { createRootRoute, createRoute, createRouter, Outlet } from '@tanstack/react-router';
|
||||
import { RootLayout } from '@/components/layout';
|
||||
import { Dashboard } from '@/pages/dashboard';
|
||||
import { SignInPage } from '@/pages/sign-in';
|
||||
import { SignUpPage } from '@/pages/sign-up';
|
||||
import { LoginPage } from '@/pages/login-page';
|
||||
|
||||
const rootRoute = createRootRoute({
|
||||
component: () => <Outlet />,
|
||||
@ -29,10 +29,10 @@ const appRoute = createRoute({
|
||||
),
|
||||
});
|
||||
|
||||
const loginRoute = createRoute({
|
||||
const signInRoute = createRoute({
|
||||
getParentRoute: () => publicRoute,
|
||||
path: '/',
|
||||
component: LoginPage,
|
||||
component: SignInPage,
|
||||
});
|
||||
|
||||
const signUpRoute = createRoute({
|
||||
@ -48,7 +48,7 @@ const dashboardRoute = createRoute({
|
||||
});
|
||||
|
||||
const routeTree = rootRoute.addChildren([
|
||||
publicRoute.addChildren([loginRoute, signUpRoute]),
|
||||
publicRoute.addChildren([signInRoute, signUpRoute]),
|
||||
appRoute.addChildren([dashboardRoute]),
|
||||
]);
|
||||
|
||||
|
||||
14
src/vite-env.d.ts
vendored
14
src/vite-env.d.ts
vendored
@ -1,14 +0,0 @@
|
||||
/// <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;
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
@ -25,7 +24,9 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
/* Path Aliases */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user