feat: improve responsiveness and refactor auth components
- Fix responsiveness at md (768px) and lg (1024px) breakpoints across all pages - Reduce forgot-password-modal.tsx from 472 to 319 lines by extracting components - Extract OTPInput and modal icons into separate files for better code organization - Optimize card sizes and text scaling for lg (1024px) breakpoint - Adjust login page layout: smaller card and reduced text sizes at lg breakpoint - Improve responsive typography, spacing, and touch targets across all components - Update all pages (login, sign-up, dashboard) with proper mobile-first responsive design
This commit is contained in:
parent
52d4aaa88b
commit
ab3bf2038b
|
Before Width: | Height: | Size: 536 B After Width: | Height: | Size: 536 B |
@ -100,13 +100,13 @@ export function AuthButton({
|
|||||||
disabled={disabled || isLoading}
|
disabled={disabled || isLoading}
|
||||||
className={`
|
className={`
|
||||||
flex items-center justify-center
|
flex items-center justify-center
|
||||||
h-[52px]
|
min-h-[44px] h-11 md:h-[52px]
|
||||||
px-[18px] py-[13px]
|
px-4 md:px-[18px] py-2.5 md:py-[13px]
|
||||||
bg-[#00E2E0]
|
bg-[#00E2E0]
|
||||||
border border-[rgba(255,255,255,0.2)]
|
border border-[rgba(255,255,255,0.2)]
|
||||||
rounded-[12px]
|
rounded-lg md:rounded-[12px]
|
||||||
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
|
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
|
||||||
text-[16px] font-semibold leading-6 text-black
|
text-sm md:text-base md:text-[16px] font-semibold leading-5 md:leading-6 text-black
|
||||||
cursor-pointer
|
cursor-pointer
|
||||||
hover:bg-[#00D4D2]
|
hover:bg-[#00D4D2]
|
||||||
active:bg-[#00C6C4]
|
active:bg-[#00C6C4]
|
||||||
@ -165,16 +165,17 @@ export function SocialButton({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`
|
className={`
|
||||||
flex items-center justify-center gap-3
|
flex items-center justify-center gap-2 md:gap-3
|
||||||
px-4 py-2.5
|
px-3 md:px-4 py-2 md:py-2.5
|
||||||
bg-white
|
bg-white
|
||||||
rounded-xl
|
rounded-lg md:rounded-xl
|
||||||
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
|
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
|
||||||
text-sm font-semibold leading-6 text-black
|
text-xs md:text-sm font-semibold leading-5 md:leading-6 text-black
|
||||||
cursor-pointer
|
cursor-pointer
|
||||||
hover:bg-gray-50
|
hover:bg-gray-50
|
||||||
active:bg-gray-100
|
active:bg-gray-100
|
||||||
transition-colors duration-200
|
transition-colors duration-200
|
||||||
|
min-h-[44px]
|
||||||
${className}
|
${className}
|
||||||
`}
|
`}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
|
|||||||
@ -53,11 +53,11 @@ export function AuthCard({ children, className = '', variant = 'signin' }: AuthC
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
flex flex-col items-center gap-8
|
flex flex-col items-center gap-5 md:gap-6 lg:gap-6 xl:gap-8
|
||||||
w-[520px] max-w-full
|
w-full max-w-[520px]
|
||||||
rounded-[32px]
|
rounded-2xl md:rounded-3xl lg:rounded-2xl xl:rounded-[32px]
|
||||||
shadow-[0px_4px_24px_0px_rgba(0,0,0,0.15)]
|
shadow-[0px_4px_24px_0px_rgba(0,0,0,0.15)]
|
||||||
${isRegister ? 'px-[42px] py-[64px]' : 'px-[60px] py-[80px]'}
|
${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}
|
${className}
|
||||||
`}
|
`}
|
||||||
style={{
|
style={{
|
||||||
@ -79,11 +79,11 @@ export function AuthCardHeader({ title, subtitle }: AuthCardHeaderProps): JSX.El
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center w-full">
|
<div className="flex flex-col items-center w-full">
|
||||||
<div className="flex flex-col items-center gap-2 w-full text-center">
|
<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">
|
<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}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
<p className="text-[18px] font-medium leading-6 text-[rgba(255,255,255,0.75)]">
|
<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}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -19,6 +19,26 @@ import {
|
|||||||
AzureIcon,
|
AzureIcon,
|
||||||
} from './index';
|
} 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
|
* Auth form mode type
|
||||||
*/
|
*/
|
||||||
@ -72,19 +92,19 @@ const nameFieldsVariants = {
|
|||||||
opacity: 0,
|
opacity: 0,
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
transition: {
|
transition: {
|
||||||
height: { ...springTransition, duration: 0.2 },
|
height: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
|
||||||
opacity: { duration: 0.1 },
|
opacity: { duration: 0.1 },
|
||||||
marginBottom: { ...springTransition, duration: 0.2 },
|
marginBottom: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
visible: {
|
visible: {
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
marginBottom: 16,
|
marginBottom: ANIMATION_TIMING.NAME_FIELDS_MARGIN,
|
||||||
transition: {
|
transition: {
|
||||||
height: { ...springTransition, duration: 0.2 },
|
height: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
|
||||||
opacity: { duration: 0.15, delay: 0.05 },
|
opacity: { duration: ANIMATION_TIMING.OPACITY_DURATION, delay: ANIMATION_TIMING.OPACITY_DELAY },
|
||||||
marginBottom: { ...springTransition, duration: 0.2 },
|
marginBottom: { ...springTransition, duration: ANIMATION_TIMING.NAME_FIELDS_DURATION },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -98,19 +118,19 @@ const forgotPasswordVariants = {
|
|||||||
opacity: 0,
|
opacity: 0,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
transition: {
|
transition: {
|
||||||
height: { ...springTransition, duration: 0.15 },
|
height: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
|
||||||
opacity: { duration: 0.1 },
|
opacity: { duration: 0.1 },
|
||||||
marginTop: { ...springTransition, duration: 0.15 },
|
marginTop: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
visible: {
|
visible: {
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
marginTop: 8,
|
marginTop: ANIMATION_TIMING.FORGOT_PASSWORD_MARGIN,
|
||||||
transition: {
|
transition: {
|
||||||
height: { ...springTransition, duration: 0.15 },
|
height: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
|
||||||
opacity: { duration: 0.15, delay: 0.05 },
|
opacity: { duration: ANIMATION_TIMING.OPACITY_DURATION, delay: ANIMATION_TIMING.OPACITY_DELAY },
|
||||||
marginTop: { ...springTransition, duration: 0.15 },
|
marginTop: { ...springTransition, duration: ANIMATION_TIMING.FORGOT_PASSWORD_DURATION },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -118,6 +138,7 @@ const forgotPasswordVariants = {
|
|||||||
/**
|
/**
|
||||||
* AuthFormCard component
|
* AuthFormCard component
|
||||||
* @description Unified authentication card with animated transitions between Sign In and Register modes.
|
* @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
|
* @param {AuthFormCardProps} props - Component props
|
||||||
* @returns {JSX.Element} AuthFormCard element
|
* @returns {JSX.Element} AuthFormCard element
|
||||||
*/
|
*/
|
||||||
@ -140,10 +161,10 @@ export function AuthFormCard({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle form submission based on current mode
|
* Handle form submission based on current mode
|
||||||
* @param {FormEvent<HTMLFormElement>} e - Form event
|
* @param {FormEvent<HTMLFormElement>} event - Form submit event
|
||||||
*/
|
*/
|
||||||
const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
|
const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
if (isRegister) {
|
if (isRegister) {
|
||||||
@ -152,11 +173,12 @@ export function AuthFormCard({
|
|||||||
onSignIn?.(email, password);
|
onSignIn?.(email, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => setIsLoading(false), 1000);
|
setTimeout(() => setIsLoading(false), ANIMATION_TIMING.FORM_SUBMIT_DELAY);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle between Sign In and Register modes
|
* Toggle between Sign In and Register modes
|
||||||
|
* Clears all form fields when switching modes
|
||||||
*/
|
*/
|
||||||
const toggleMode = (): void => {
|
const toggleMode = (): void => {
|
||||||
setMode((prev) => (prev === 'signin' ? 'register' : 'signin'));
|
setMode((prev) => (prev === 'signin' ? 'register' : 'signin'));
|
||||||
@ -169,8 +191,8 @@ export function AuthFormCard({
|
|||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
layout
|
layout
|
||||||
transition={{ layout: { ...springTransition, duration: 0.3 } }}
|
transition={{ layout: { ...springTransition, duration: ANIMATION_TIMING.LAYOUT_DURATION } }}
|
||||||
className="w-[520px] max-w-full"
|
className="w-full max-w-[460px] lg:max-w-[440px] xl:max-w-[520px]"
|
||||||
style={{ willChange: 'transform' }}
|
style={{ willChange: 'transform' }}
|
||||||
>
|
>
|
||||||
<AuthCard variant={isRegister ? 'register' : 'signin'}>
|
<AuthCard variant={isRegister ? 'register' : 'signin'}>
|
||||||
@ -186,7 +208,7 @@ export function AuthFormCard({
|
|||||||
|
|
||||||
{/* Form Content */}
|
{/* Form Content */}
|
||||||
<AuthCardContent>
|
<AuthCardContent>
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4 w-full">
|
<form onSubmit={handleSubmit} className="flex flex-col gap-3 md:gap-4 lg:gap-3 xl:gap-4 w-full">
|
||||||
{/* Collapsible Name Fields Row */}
|
{/* Collapsible Name Fields Row */}
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={false}>
|
||||||
{isRegister && (
|
{isRegister && (
|
||||||
|
|||||||
@ -134,7 +134,7 @@ export const AuthInput = forwardRef<HTMLInputElement, AuthInputProps>(
|
|||||||
className="
|
className="
|
||||||
flex-1 min-w-0
|
flex-1 min-w-0
|
||||||
bg-transparent
|
bg-transparent
|
||||||
text-base font-medium leading-6
|
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
|
text-white
|
||||||
placeholder:text-[rgba(255,255,255,0.5)]
|
placeholder:text-[rgba(255,255,255,0.5)]
|
||||||
outline-none
|
outline-none
|
||||||
@ -149,7 +149,7 @@ export const AuthInput = forwardRef<HTMLInputElement, AuthInputProps>(
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={togglePasswordVisibility}
|
onClick={togglePasswordVisibility}
|
||||||
className="flex-shrink-0 cursor-pointer hover:opacity-80 transition-opacity"
|
className="shrink-0 cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||||
>
|
>
|
||||||
{showPassword ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
{showPassword ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||||
|
|||||||
@ -7,9 +7,22 @@
|
|||||||
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
|
* Follows AgenticIQ Enterprise Coding Guidelines v1.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useRef, useEffect, type FormEvent, type KeyboardEvent, type ClipboardEvent } from 'react';
|
import { useState, type FormEvent } from 'react';
|
||||||
import { motion, AnimatePresence, type Easing } from 'framer-motion';
|
import { motion, AnimatePresence, type Easing } from 'framer-motion';
|
||||||
import { AuthInput, AuthButton } from './index';
|
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
|
* Modal step type - determines which view to show
|
||||||
@ -30,126 +43,16 @@ interface ForgotPasswordModalProps {
|
|||||||
onVerify?: (otp: string) => void;
|
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)
|
* Smooth easing curve for animations (cubic-bezier)
|
||||||
|
* Material Design standard easing
|
||||||
*/
|
*/
|
||||||
const smoothEasing: Easing = [0.4, 0, 0.2, 1];
|
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
|
* ForgotPasswordModal component
|
||||||
* @description Modal with smooth transitions between email input and OTP verification.
|
* @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
|
* @param {ForgotPasswordModalProps} props - Component props
|
||||||
* @returns {JSX.Element} ForgotPasswordModal element
|
* @returns {JSX.Element} ForgotPasswordModal element
|
||||||
*/
|
*/
|
||||||
@ -164,6 +67,9 @@ export function ForgotPasswordModal({
|
|||||||
const [otp, setOtp] = useState<string[]>(['', '', '', '', '', '']);
|
const [otp, setOtp] = useState<string[]>(['', '', '', '', '', '']);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle modal close and reset state
|
||||||
|
*/
|
||||||
const handleClose = (): void => {
|
const handleClose = (): void => {
|
||||||
onClose();
|
onClose();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -171,38 +77,58 @@ export function ForgotPasswordModal({
|
|||||||
setEmail('');
|
setEmail('');
|
||||||
setOtp(['', '', '', '', '', '']);
|
setOtp(['', '', '', '', '', '']);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}, 250);
|
}, ANIMATION_TIMING.MODAL_RESET_DELAY);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEmailSubmit = (e: FormEvent<HTMLFormElement>): void => {
|
/**
|
||||||
e.preventDefault();
|
* Handle email form submission
|
||||||
|
* @param {FormEvent<HTMLFormElement>} event - Form submit event
|
||||||
|
*/
|
||||||
|
const handleEmailSubmit = (event: FormEvent<HTMLFormElement>): void => {
|
||||||
|
event.preventDefault();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
onSubmit?.(email);
|
onSubmit?.(email);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setStep('verify');
|
setStep('verify');
|
||||||
}, 600);
|
}, ANIMATION_TIMING.EMAIL_SUBMIT_DELAY);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVerifySubmit = (e: FormEvent<HTMLFormElement>): void => {
|
/**
|
||||||
e.preventDefault();
|
* Handle OTP verification form submission
|
||||||
|
* @param {FormEvent<HTMLFormElement>} event - Form submit event
|
||||||
|
*/
|
||||||
|
const handleVerifySubmit = (event: FormEvent<HTMLFormElement>): void => {
|
||||||
|
event.preventDefault();
|
||||||
const otpString = otp.join('');
|
const otpString = otp.join('');
|
||||||
if (otpString.length !== 6) return;
|
if (otpString.length !== ANIMATION_TIMING.OTP_LENGTH) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
onVerify?.(otpString);
|
onVerify?.(otpString);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
handleClose();
|
handleClose();
|
||||||
}, 800);
|
}, ANIMATION_TIMING.OTP_VERIFY_DELAY);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle resend code request
|
||||||
|
*/
|
||||||
const handleResendCode = (): void => {
|
const handleResendCode = (): void => {
|
||||||
console.log('Resend code to:', email);
|
// TODO(AUTH-007): Implement resend OTP API call
|
||||||
|
onSubmit?.(email);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle backdrop click to close modal
|
||||||
|
*/
|
||||||
const handleBackdropClick = (): void => handleClose();
|
const handleBackdropClick = (): void => handleClose();
|
||||||
const handleModalClick = (e: React.MouseEvent): void => e.stopPropagation();
|
|
||||||
|
/**
|
||||||
|
* 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';
|
const isEmailStep = step === 'email';
|
||||||
|
|
||||||
@ -216,7 +142,7 @@ export function ForgotPasswordModal({
|
|||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.25, ease: smoothEasing }}
|
transition={{ duration: ANIMATION_TIMING.TRANSITION_DURATION, ease: smoothEasing }}
|
||||||
onClick={handleBackdropClick}
|
onClick={handleBackdropClick}
|
||||||
className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm"
|
className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm"
|
||||||
/>
|
/>
|
||||||
@ -227,21 +153,22 @@ export function ForgotPasswordModal({
|
|||||||
initial={{ opacity: 0, scale: 0.96, y: 10 }}
|
initial={{ opacity: 0, scale: 0.96, y: 10 }}
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, scale: 0.96, y: 10 }}
|
exit={{ opacity: 0, scale: 0.96, y: 10 }}
|
||||||
transition={{ duration: 0.25, ease: smoothEasing }}
|
transition={{ duration: ANIMATION_TIMING.TRANSITION_DURATION, ease: smoothEasing }}
|
||||||
onClick={handleBackdropClick}
|
onClick={handleBackdropClick}
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
>
|
>
|
||||||
{/* Modal Card */}
|
{/* Modal Card */}
|
||||||
<motion.div
|
<motion.div
|
||||||
layout
|
layout
|
||||||
transition={{ duration: 0.3, ease: smoothEasing }}
|
transition={{ duration: ANIMATION_TIMING.LAYOUT_DURATION, ease: smoothEasing }}
|
||||||
onClick={handleModalClick}
|
onClick={handleModalClick}
|
||||||
className="
|
className="
|
||||||
flex flex-col items-center gap-8
|
flex flex-col items-center gap-6 md:gap-7 lg:gap-8
|
||||||
w-[520px] max-w-full
|
w-full max-w-[520px]
|
||||||
p-[42px]
|
p-6 md:p-8 lg:p-[42px]
|
||||||
rounded-[32px]
|
rounded-2xl md:rounded-3xl lg:rounded-[32px]
|
||||||
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
|
shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]
|
||||||
|
mx-4
|
||||||
"
|
"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(180deg, #00B8B7 0%, #001C8E 100%)',
|
background: 'linear-gradient(180deg, #00B8B7 0%, #001C8E 100%)',
|
||||||
@ -257,14 +184,14 @@ export function ForgotPasswordModal({
|
|||||||
initial={{ opacity: 0, y: -5 }}
|
initial={{ opacity: 0, y: -5 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: 5 }}
|
exit={{ opacity: 0, y: 5 }}
|
||||||
transition={{ duration: 0.2, ease: smoothEasing }}
|
transition={{ duration: ANIMATION_TIMING.HEADER_DURATION, ease: smoothEasing }}
|
||||||
>
|
>
|
||||||
<h2 className="text-[24px] font-semibold leading-[38px] text-white">
|
<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'}
|
{isEmailStep ? 'Forgot your password' : 'Verify Your Email'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className={isEmailStep
|
<p className={isEmailStep
|
||||||
? "text-[24px] font-semibold leading-[38px] text-white"
|
? "text-xl md:text-xl lg:text-2xl xl:text-[24px] font-semibold leading-tight md:leading-tight lg:leading-[38px] text-white"
|
||||||
: "text-[18px] font-medium leading-6 text-[rgba(255,255,255,0.75)] mt-1"
|
: "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'}
|
{isEmailStep ? 'and continue' : 'Enter the 6-Digit Verification Code'}
|
||||||
</p>
|
</p>
|
||||||
@ -278,11 +205,11 @@ export function ForgotPasswordModal({
|
|||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
className="
|
className="
|
||||||
flex items-center justify-center
|
flex items-center justify-center
|
||||||
w-8 h-8 rounded-full
|
w-8 h-8 md:w-10 md:h-10 rounded-full
|
||||||
bg-[rgba(255,255,255,0.1)]
|
bg-[rgba(255,255,255,0.1)]
|
||||||
hover:bg-[rgba(255,255,255,0.2)]
|
hover:bg-[rgba(255,255,255,0.2)]
|
||||||
transition-colors duration-200
|
transition-colors duration-200
|
||||||
cursor-pointer shrink-0
|
cursor-pointer shrink-0 min-w-[44px] min-h-[44px]
|
||||||
"
|
"
|
||||||
aria-label="Close modal"
|
aria-label="Close modal"
|
||||||
>
|
>
|
||||||
@ -299,7 +226,7 @@ export function ForgotPasswordModal({
|
|||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
exit={{ opacity: 0, x: 20 }}
|
exit={{ opacity: 0, x: 20 }}
|
||||||
transition={{ duration: 0.25, ease: smoothEasing }}
|
transition={{ duration: ANIMATION_TIMING.TRANSITION_DURATION, ease: smoothEasing }}
|
||||||
onSubmit={handleEmailSubmit}
|
onSubmit={handleEmailSubmit}
|
||||||
className="flex flex-col gap-6 w-full"
|
className="flex flex-col gap-6 w-full"
|
||||||
>
|
>
|
||||||
@ -322,14 +249,14 @@ export function ForgotPasswordModal({
|
|||||||
initial={{ opacity: 0, x: 20 }}
|
initial={{ opacity: 0, x: 20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
exit={{ opacity: 0, x: -20 }}
|
exit={{ opacity: 0, x: -20 }}
|
||||||
transition={{ duration: 0.25, ease: smoothEasing }}
|
transition={{ duration: ANIMATION_TIMING.TRANSITION_DURATION, ease: smoothEasing }}
|
||||||
onSubmit={handleVerifySubmit}
|
onSubmit={handleVerifySubmit}
|
||||||
className="flex flex-col gap-6 w-full items-center"
|
className="flex flex-col gap-6 w-full items-center"
|
||||||
>
|
>
|
||||||
<OTPInput otp={otp} setOtp={setOtp} />
|
<OTPInput otp={otp} setOtp={setOtp} />
|
||||||
<AuthButton
|
<AuthButton
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
disabled={otp.join('').length !== 6}
|
disabled={otp.join('').length !== ANIMATION_TIMING.OTP_LENGTH}
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
>
|
||||||
Continue
|
Continue
|
||||||
@ -347,7 +274,7 @@ export function ForgotPasswordModal({
|
|||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.2, ease: smoothEasing }}
|
transition={{ duration: ANIMATION_TIMING.HEADER_DURATION, ease: smoothEasing }}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
className="
|
className="
|
||||||
@ -355,6 +282,7 @@ export function ForgotPasswordModal({
|
|||||||
text-sm font-semibold text-white
|
text-sm font-semibold text-white
|
||||||
cursor-pointer hover:opacity-80
|
cursor-pointer hover:opacity-80
|
||||||
transition-opacity duration-200
|
transition-opacity duration-200
|
||||||
|
min-h-[44px]
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon />
|
<ArrowLeftIcon />
|
||||||
@ -366,16 +294,16 @@ export function ForgotPasswordModal({
|
|||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.2, ease: smoothEasing }}
|
transition={{ duration: ANIMATION_TIMING.HEADER_DURATION, ease: smoothEasing }}
|
||||||
className="flex items-center justify-center gap-1"
|
className="flex flex-col sm:flex-row items-center justify-center gap-1 text-center sm:text-left"
|
||||||
>
|
>
|
||||||
<span className="text-sm font-normal text-[rgba(255,255,255,0.5)]">
|
<span className="text-xs sm:text-sm font-normal text-[rgba(255,255,255,0.5)]">
|
||||||
Didn't you receive any code?
|
Didn't you receive any code?
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleResendCode}
|
onClick={handleResendCode}
|
||||||
className="text-sm font-semibold text-white cursor-pointer hover:underline"
|
className="text-xs sm:text-sm font-semibold text-white cursor-pointer hover:underline min-h-[44px]"
|
||||||
>
|
>
|
||||||
Resend Code
|
Resend Code
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -9,4 +9,6 @@ export { AuthInput } from './auth-input';
|
|||||||
export { AuthButton, SocialButton, TextButton, GoogleIcon, AzureIcon } from './auth-button';
|
export { AuthButton, SocialButton, TextButton, GoogleIcon, AzureIcon } from './auth-button';
|
||||||
export { AuthFormCard } from './auth-form-card';
|
export { AuthFormCard } from './auth-form-card';
|
||||||
export { ForgotPasswordModal } from './forgot-password-modal';
|
export { ForgotPasswordModal } from './forgot-password-modal';
|
||||||
|
export { OTPInput } from './otp-input';
|
||||||
|
export { CloseIcon, ArrowLeftIcon } from './modal-icons';
|
||||||
|
|
||||||
|
|||||||
32
src/components/auth/modal-icons.tsx
Normal file
32
src/components/auth/modal-icons.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
139
src/components/auth/otp-input.tsx
Normal file
139
src/components/auth/otp-input.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* 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 (
|
return (
|
||||||
<div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100 card-hover">
|
<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-lg font-semibold text-gray-900 mb-3">{title}</h3>
|
<h3 className="text-base sm:text-lg font-semibold text-gray-900 mb-3">{title}</h3>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2 mb-4">
|
<div className="flex flex-wrap gap-2 mb-4">
|
||||||
{tags.map((tag, index) => (
|
{tags.map((tag, index) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
/* Use the index to pick the color; fall back to the first color if index > 2 */
|
/* Use the index to pick the color; fall back to the first color if index > 2 */
|
||||||
className={`${tagColors[index] || tagColors[0]} px-3 py-1 text-xs font-medium text-gray-700 border border-gray-200`}
|
className={`${tagColors[index] || tagColors[0]} px-2 sm:px-3 py-1 text-xs font-medium text-gray-700 border border-gray-200 rounded`}
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -51,17 +51,17 @@ export function MetricCard({ title, subtitle, value, gradient }: MetricCardProps
|
|||||||
// </div>
|
// </div>
|
||||||
// );
|
// );
|
||||||
return (
|
return (
|
||||||
<div className={`p-6 h-[110px] rounded-2xl ${gradients[gradient]} w-full `}>
|
<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="flex items-center justify-between w-full">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-[12px] font-medium text-white uppercase tracking-wider">
|
<span className="text-[10px] sm:text-[12px] font-medium text-white uppercase tracking-wider">
|
||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[18px] font-bold mt-1 text-white">
|
<span className="text-base sm:text-lg md:text-[18px] font-bold mt-1 text-white">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-5xl font-bold text-white">
|
<div className="text-3xl sm:text-4xl md:text-5xl font-bold text-white">
|
||||||
{value}
|
{value}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,47 +12,47 @@ import { Home, Settings } from 'lucide-react';
|
|||||||
*/
|
*/
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
return (
|
return (
|
||||||
<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">
|
<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">
|
||||||
{/* Navigation Icons */}
|
{/* Navigation Icons */}
|
||||||
<nav className="flex flex-col items-center gap-5 flex-1 w-full">
|
<nav className="flex flex-col items-center gap-3 sm:gap-4 md:gap-5 flex-1 w-full">
|
||||||
{/* Home - Active State */}
|
{/* Home - Active State */}
|
||||||
<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">
|
<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-5 h-5 text-white" />
|
<Home className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button className="sidebar-icon-v3" aria-label="Agent">
|
<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-[42px] h-[42px] " />
|
<img src="/Agent.svg" alt="Agent" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||||
</button>
|
</button>
|
||||||
<button className="sidebar-icon-v3" aria-label="KB">
|
<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-[42px] h-[42px] " />
|
<img src="/KB.svg" alt="KB" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||||
</button>
|
</button>
|
||||||
<button className="sidebar-icon-v3" aria-label="Models">
|
<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-[42px] h-[42px] " />
|
<img src="/Models.svg" alt="Models" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||||
</button>
|
</button>
|
||||||
<button className="sidebar-icon-v3" aria-label="Workflow">
|
<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-[42px] h-[42px] " />
|
<img src="/Workflow.svg" alt="Workflow" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||||
</button>
|
</button>
|
||||||
<button className="sidebar-icon-v3" aria-label="Nav Item">
|
<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-[42px] h-[42px] " />
|
<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>
|
</button>
|
||||||
<button className="sidebar-icon-v3" aria-label="LLM">
|
<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-[42px] h-[42px] " />
|
<img src="/LLM.svg" alt="LLM" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||||
</button>
|
</button>
|
||||||
<button className="sidebar-icon-v3" aria-label="FT">
|
<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-[42px] h-[42px] " />
|
<img src="/FT.svg" alt="FT" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||||
</button>
|
</button>
|
||||||
<button className="sidebar-icon-v3" aria-label="Task Watcher">
|
<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-[42px] h-[42px] " />
|
<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>
|
</button>
|
||||||
<button className="sidebar-icon-v3" aria-label="Optimization">
|
<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-[42px] h-[42px] " />
|
<img src="/Optimization.svg" alt="Optimization" className="w-10 h-10 sm:w-11 sm:h-11 md:w-[42px] md:h-[42px]" />
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Bottom Icon */}
|
{/* Bottom Icon */}
|
||||||
<div className="flex flex-col gap-4 mt-auto">
|
<div className="flex flex-col gap-3 sm:gap-4 mt-auto">
|
||||||
<button className="sidebar-icon-v3" aria-label="Settings">
|
<button className="sidebar-icon-v3 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Settings">
|
||||||
<Settings className="w-5 h-5 text-white" />
|
<Settings className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export function Header() {
|
|||||||
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
<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">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img
|
<img
|
||||||
@ -23,51 +23,51 @@ export function Header() {
|
|||||||
alt="AgenticIQ Logo"
|
alt="AgenticIQ Logo"
|
||||||
width={168}
|
width={168}
|
||||||
height={60}
|
height={60}
|
||||||
className="object-contain"
|
className="object-contain h-8 sm:h-10 md:h-auto w-auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Actions */}
|
{/* Right Actions */}
|
||||||
<div className="bg-white rounded-[20px] p-2 flex items-center gap-2 shadow-sm border border-gray-100/50">
|
<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">
|
||||||
{/* Bell Notification */}
|
{/* Bell Notification */}
|
||||||
<button className="p-2.5 bg-[#F4F9FF] hover:bg-[#E9F3FF] rounded-xl transition-all duration-200">
|
<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-5 h-5 text-[#1A1A1A]" strokeWidth={1.5} />
|
<Bell className="w-4 h-4 sm:w-5 sm:h-5 text-[#1A1A1A]" strokeWidth={1.5} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Theme Toggles */}
|
{/* Theme Toggles */}
|
||||||
<div className="flex items-center gap-1.5 px-1">
|
<div className="flex items-center gap-1 sm:gap-1.5 px-0.5 sm:px-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setTheme('light')}
|
onClick={() => setTheme('light')}
|
||||||
className={`p-2.5 rounded-full transition-all duration-300 ${
|
className={`p-2 sm:p-2.5 rounded-full transition-all duration-300 min-w-[44px] min-h-[44px] flex items-center justify-center ${
|
||||||
theme === 'light'
|
theme === 'light'
|
||||||
? 'bg-[#0033FF] text-white shadow-lg shadow-blue-500/40 scale-105'
|
? 'bg-[#0033FF] text-white shadow-lg shadow-blue-500/40 scale-105'
|
||||||
: 'bg-[#F4F9FF] text-gray-500 hover:text-gray-700'
|
: 'bg-[#F4F9FF] text-gray-500 hover:text-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Sun className="w-5 h-5" strokeWidth={theme === 'light' ? 2 : 1.5} />
|
<Sun className="w-4 h-4 sm:w-5 sm:h-5" strokeWidth={theme === 'light' ? 2 : 1.5} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setTheme('dark')}
|
onClick={() => setTheme('dark')}
|
||||||
className={`p-2.5 rounded-full transition-all duration-300 ${
|
className={`p-2 sm:p-2.5 rounded-full transition-all duration-300 min-w-[44px] min-h-[44px] flex items-center justify-center ${
|
||||||
theme === 'dark'
|
theme === 'dark'
|
||||||
? 'bg-[#0052FF] text-white shadow-lg shadow-blue-500/40 scale-105'
|
? 'bg-[#0052FF] text-white shadow-lg shadow-blue-500/40 scale-105'
|
||||||
: 'bg-[#F4F9FF] text-gray-500 hover:text-gray-700'
|
: 'bg-[#F4F9FF] text-gray-500 hover:text-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Moon className="w-5 h-5" strokeWidth={theme === 'dark' ? 2 : 1.5} />
|
<Moon className="w-4 h-4 sm:w-5 sm:h-5" strokeWidth={theme === 'dark' ? 2 : 1.5} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Profile Section */}
|
{/* Profile Section */}
|
||||||
<div className="flex items-center gap-2 pl-1 pr-2">
|
<div className="flex items-center gap-1 sm:gap-2 pl-0.5 sm:pl-1 pr-1 sm:pr-2">
|
||||||
<div className="w-10 h-10 rounded-full overflow-hidden border-2 border-white shadow-sm ring-1 ring-gray-100">
|
<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">
|
||||||
<img
|
<img
|
||||||
src="/profile.svg"
|
src="/profile.svg"
|
||||||
alt="User Avatar"
|
alt="User Avatar"
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown className="w-4 h-4 text-gray-900" strokeWidth={2.5} />
|
<ChevronDown className="w-3 h-3 sm:w-4 sm:h-4 text-gray-900 hidden sm:block" strokeWidth={2.5} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export function RootLayout({ children }: RootLayoutProps) {
|
|||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<main className="ml-24 pt-16 min-h-screen">
|
<main className="md:ml-20 lg:ml-24 pt-14 sm:pt-16 md:pt-16 lg:pt-16 min-h-screen">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -51,37 +51,38 @@ export function Dashboard() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-8 pt-8">
|
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6 md:pt-8">
|
||||||
{/* Content Header: Welcome + Search/Create */}
|
{/* Content Header: Welcome + Search/Create */}
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6 md:mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-[22px] font-semibold text-gray-900 mb-2">
|
<h1 className="text-xl sm:text-[22px] font-semibold text-gray-900 mb-2">
|
||||||
Welcome, Vijay! 👋
|
Welcome, Vijay! 👋
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-500 max-w-2xl text-[16px]">
|
<p className="text-gray-500 max-w-2xl text-sm sm:text-base md:text-[16px]">
|
||||||
Monitor and manage your intelligent agents, knowledge bases, and automated workflows from a centralized dashboard
|
Monitor and manage your intelligent agents, knowledge bases, and automated workflows from a centralized dashboard
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 w-full sm:w-auto">
|
||||||
<div className="relative group">
|
<div className="relative group flex-1 sm:flex-initial">
|
||||||
<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" />
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search"
|
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-64 shadow-sm transition-all"
|
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]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<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">
|
||||||
<Plus className="w-5 h-5" />
|
<Plus className="w-5 h-5" />
|
||||||
Create Agent
|
<span className="hidden sm:inline">Create Agent</span>
|
||||||
|
<span className="sm:hidden">Create</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Metrics Grid */}
|
{/* Metrics Grid */}
|
||||||
<div className="bg-[#F3F8FF]/80 backdrop-blur-sm rounded-[32px] p-6 mb-8 border border-white/40 shadow-sm">
|
<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 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="ACTIVE AGENTS"
|
title="ACTIVE AGENTS"
|
||||||
subtitle="Agents"
|
subtitle="Agents"
|
||||||
@ -110,12 +111,12 @@ export function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Agents Section */}
|
{/* Agents Section */}
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6">
|
<div className="bg-white rounded-xl md:rounded-2xl shadow-sm border border-gray-200 p-4 sm:p-6">
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="flex gap-4 mb-6">
|
<div className="flex gap-2 sm:gap-4 mb-4 sm:mb-6 overflow-x-auto">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('all')}
|
onClick={() => setActiveTab('all')}
|
||||||
className={`px-6 py-2 rounded-lg text font-medium transition-colors ${activeTab === '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'
|
||||||
? 'bg-black text-white'
|
? 'bg-black text-white'
|
||||||
: 'bg-white text-gray-600 border border-gray-300 hover:bg-gray-50'
|
: 'bg-white text-gray-600 border border-gray-300 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
@ -124,7 +125,7 @@ export function Dashboard() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('my')}
|
onClick={() => setActiveTab('my')}
|
||||||
className={`px-6 py-2 rounded-lg font-medium transition-colors ${activeTab === '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'
|
||||||
? 'bg-black text-white'
|
? 'bg-black text-white'
|
||||||
: 'bg-white text-gray-600 border border-gray-300 hover:bg-gray-50'
|
: 'bg-white text-gray-600 border border-gray-300 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
@ -134,7 +135,7 @@ export function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Agent Cards Grid */}
|
{/* Agent Cards Grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<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">
|
||||||
{agents.map((agent, index) => (
|
{agents.map((agent, index) => (
|
||||||
<AgentCard
|
<AgentCard
|
||||||
key={index}
|
key={index}
|
||||||
|
|||||||
@ -15,6 +15,16 @@ import bottomLeftWave2 from '@/assets/images/backgrounds/bottom-left-wave2.svg';
|
|||||||
import logoGlowBg from '@/assets/images/backgrounds/logo-glow-bg.svg';
|
import logoGlowBg from '@/assets/images/backgrounds/logo-glow-bg.svg';
|
||||||
import { AuthFormCard, ForgotPasswordModal } from '@/components/auth';
|
import { AuthFormCard, ForgotPasswordModal } from '@/components/auth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register form data interface
|
||||||
|
*/
|
||||||
|
interface RegisterFormData {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LoginPage component
|
* LoginPage component
|
||||||
* @description Full-page login layout with responsive 2-column grid structure.
|
* @description Full-page login layout with responsive 2-column grid structure.
|
||||||
@ -24,41 +34,69 @@ import { AuthFormCard, ForgotPasswordModal } from '@/components/auth';
|
|||||||
export function LoginPage(): JSX.Element {
|
export function LoginPage(): JSX.Element {
|
||||||
const [showForgotPwd, setShowForgotPwd] = useState(false);
|
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 => {
|
const handleSignIn = (email: string, password: string): void => {
|
||||||
console.log('Sign in:', { email, password });
|
// TODO(AUTH-001): Implement actual sign in API call
|
||||||
|
void Promise.resolve({ email, password });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRegister = (data: {
|
/**
|
||||||
firstName: string;
|
* Handle register form submission
|
||||||
lastName: string;
|
* @param {RegisterFormData} data - Registration form data
|
||||||
email: string;
|
*/
|
||||||
password: string;
|
const handleRegister = (data: RegisterFormData): void => {
|
||||||
}): void => {
|
// TODO(AUTH-002): Implement actual registration API call
|
||||||
console.log('Register:', data);
|
void Promise.resolve(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Google OAuth authentication
|
||||||
|
*/
|
||||||
const handleGoogleAuth = (): void => {
|
const handleGoogleAuth = (): void => {
|
||||||
console.log('Google auth clicked');
|
// TODO(AUTH-003): Implement Google OAuth flow
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Azure OAuth authentication
|
||||||
|
*/
|
||||||
const handleAzureAuth = (): void => {
|
const handleAzureAuth = (): void => {
|
||||||
console.log('Azure auth clicked');
|
// TODO(AUTH-004): Implement Azure OAuth flow
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open forgot password modal
|
||||||
|
*/
|
||||||
const handleForgotPasswordClick = (): void => {
|
const handleForgotPasswordClick = (): void => {
|
||||||
setShowForgotPwd(true);
|
setShowForgotPwd(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close forgot password modal
|
||||||
|
*/
|
||||||
const handleForgotPasswordClose = (): void => {
|
const handleForgotPasswordClose = (): void => {
|
||||||
setShowForgotPwd(false);
|
setShowForgotPwd(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle forgot password email submission
|
||||||
|
* @param {string} email - Email for password recovery
|
||||||
|
*/
|
||||||
const handleForgotPasswordSubmit = (email: string): void => {
|
const handleForgotPasswordSubmit = (email: string): void => {
|
||||||
console.log('Password recovery requested for:', email);
|
// 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 => {
|
const handleOtpVerify = (otp: string): void => {
|
||||||
console.log('OTP verification:', otp);
|
// TODO(AUTH-006): Implement OTP verification API call
|
||||||
|
void Promise.resolve(otp);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -68,11 +106,11 @@ export function LoginPage(): JSX.Element {
|
|||||||
<BackgroundDecorations />
|
<BackgroundDecorations />
|
||||||
|
|
||||||
{/* AgenticIQ Logo */}
|
{/* AgenticIQ Logo */}
|
||||||
<div className="absolute left-[80px] top-[54px] z-20">
|
<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
|
<img
|
||||||
src={agenticiqLogo}
|
src={agenticiqLogo}
|
||||||
alt="AgenticIQ Logo"
|
alt="AgenticIQ Logo"
|
||||||
className="h-[117px] w-auto"
|
className="h-16 w-auto md:h-24 lg:h-20 xl:h-[117px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -80,12 +118,12 @@ export function LoginPage(): JSX.Element {
|
|||||||
<LeftColumnText />
|
<LeftColumnText />
|
||||||
|
|
||||||
{/* Main Content Grid */}
|
{/* 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]">
|
<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 */}
|
{/* Left Column - Branding/Marketing Content */}
|
||||||
<LeftColumn />
|
<LeftColumn />
|
||||||
|
|
||||||
{/* Right Column - Auth Form Card */}
|
{/* Right Column - Auth Form Card */}
|
||||||
<div className="flex items-center justify-start px-6 py-12 lg:px-0 lg:py-16">
|
<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
|
<AuthFormCard
|
||||||
initialMode="signin"
|
initialMode="signin"
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
@ -112,6 +150,7 @@ export function LoginPage(): JSX.Element {
|
|||||||
/**
|
/**
|
||||||
* BackgroundDecorations component
|
* BackgroundDecorations component
|
||||||
* @description Renders decorative background images matching Figma design.
|
* @description Renders decorative background images matching Figma design.
|
||||||
|
* All images are purely decorative and hidden from screen readers.
|
||||||
* @returns {JSX.Element} Background decoration elements
|
* @returns {JSX.Element} Background decoration elements
|
||||||
*/
|
*/
|
||||||
function BackgroundDecorations(): JSX.Element {
|
function BackgroundDecorations(): JSX.Element {
|
||||||
@ -121,7 +160,7 @@ function BackgroundDecorations(): JSX.Element {
|
|||||||
<img
|
<img
|
||||||
src={logoGlowBg}
|
src={logoGlowBg}
|
||||||
alt=""
|
alt=""
|
||||||
className="absolute left-0 top-0 h-auto w-auto opacity-[0.08]"
|
className="absolute left-0 top-0 h-auto w-auto opacity-[0.08] hidden md:block"
|
||||||
style={{ width: '550px', height: 'auto', maxHeight: '350px' }}
|
style={{ width: '550px', height: 'auto', maxHeight: '350px' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -129,7 +168,7 @@ function BackgroundDecorations(): JSX.Element {
|
|||||||
<img
|
<img
|
||||||
src={topRightGlow}
|
src={topRightGlow}
|
||||||
alt=""
|
alt=""
|
||||||
className="absolute right-0 top-0 h-auto w-auto opacity-[0.15]"
|
className="absolute right-0 top-0 h-auto w-auto opacity-[0.15] hidden md:block"
|
||||||
style={{ width: '515px', height: 'auto', maxHeight: '614px' }}
|
style={{ width: '515px', height: 'auto', maxHeight: '614px' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -137,7 +176,7 @@ function BackgroundDecorations(): JSX.Element {
|
|||||||
<img
|
<img
|
||||||
src={bottomLeftWave1}
|
src={bottomLeftWave1}
|
||||||
alt=""
|
alt=""
|
||||||
className="absolute bottom-0 left-0 h-auto w-auto opacity-[0.15]"
|
className="absolute bottom-0 left-0 h-auto w-auto opacity-[0.15] hidden md:block"
|
||||||
style={{ width: '559px', height: 'auto', maxHeight: '613px' }}
|
style={{ width: '559px', height: 'auto', maxHeight: '613px' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -145,7 +184,7 @@ function BackgroundDecorations(): JSX.Element {
|
|||||||
<img
|
<img
|
||||||
src={bottomLeftWave2}
|
src={bottomLeftWave2}
|
||||||
alt=""
|
alt=""
|
||||||
className="absolute bottom-0 left-0 h-auto w-auto opacity-[0.15]"
|
className="absolute bottom-0 left-0 h-auto w-auto opacity-[0.15] hidden md:block"
|
||||||
style={{ width: '350px', height: 'auto', maxHeight: '500px' }}
|
style={{ width: '350px', height: 'auto', maxHeight: '500px' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -154,21 +193,20 @@ function BackgroundDecorations(): JSX.Element {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* LeftColumnText component
|
* LeftColumnText component
|
||||||
* @description Left side text content (heading and description).
|
* @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
|
* @returns {JSX.Element} Text elements positioned as per Figma
|
||||||
*/
|
*/
|
||||||
function LeftColumnText(): JSX.Element {
|
function LeftColumnText(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1
|
<h1
|
||||||
className="absolute text-[36px] font-semibold text-black leading-normal whitespace-pre-wrap z-20"
|
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]"
|
||||||
style={{ left: '110px', top: '348px', width: '513px' }}
|
|
||||||
>
|
>
|
||||||
Engineering the Future with Intelligent Agents
|
Engineering the Future with Intelligent Agents
|
||||||
</h1>
|
</h1>
|
||||||
<p
|
<p
|
||||||
className="absolute text-[24px] font-normal text-[rgba(0,0,0,0.75)] leading-normal whitespace-pre-wrap z-20"
|
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]"
|
||||||
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.
|
Deploy intelligent agents that automate complex workflows, enhance decision-making, and scale your operations. Built for enterprise reliability with seamless integration capabilities.
|
||||||
</p>
|
</p>
|
||||||
@ -178,12 +216,13 @@ function LeftColumnText(): JSX.Element {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* LeftColumn component
|
* LeftColumn component
|
||||||
* @description Empty container for left side content.
|
* @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
|
* @returns {JSX.Element} Left column container
|
||||||
*/
|
*/
|
||||||
function LeftColumn(): JSX.Element {
|
function LeftColumn(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center px-6 py-12 sm:px-12 md:px-16 lg:px-20 xl:px-28">
|
<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 */}
|
{/* Text content is positioned absolutely on page level */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,12 +7,12 @@ import { Link } from '@tanstack/react-router';
|
|||||||
|
|
||||||
export function SignUpPage() {
|
export function SignUpPage() {
|
||||||
return (
|
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="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-2xl bg-white p-8 shadow-xl">
|
<div className="w-full max-w-md rounded-xl md:rounded-2xl bg-white p-6 sm:p-8 shadow-xl">
|
||||||
<div className="mb-6 text-center">
|
<div className="mb-6 text-center">
|
||||||
<img src="/Logo.png" alt="AgenticIQ Logo" className="mx-auto h-10 w-auto" />
|
<img src="/Logo.png" alt="AgenticIQ Logo" className="mx-auto h-8 w-auto sm:h-10" />
|
||||||
<h1 className="mt-4 text-2xl font-semibold text-gray-900">Create your account</h1>
|
<h1 className="mt-4 text-xl sm: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>
|
<p className="mt-1 text-xs sm:text-sm text-gray-500">Join AgenticIQ to manage your agents and workflows.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form className="space-y-4">
|
<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>
|
<label className="mb-1 block text-sm font-medium text-gray-700">Full name</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
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"
|
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]"
|
||||||
placeholder="Jane Doe"
|
placeholder="Jane Doe"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -28,7 +28,7 @@ export function SignUpPage() {
|
|||||||
<label className="mb-1 block text-sm font-medium text-gray-700">Email</label>
|
<label className="mb-1 block text-sm font-medium text-gray-700">Email</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
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"
|
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]"
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -36,19 +36,19 @@ export function SignUpPage() {
|
|||||||
<label className="mb-1 block text-sm font-medium text-gray-700">Password</label>
|
<label className="mb-1 block text-sm font-medium text-gray-700">Password</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
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"
|
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]"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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]"
|
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]"
|
||||||
>
|
>
|
||||||
Sign Up
|
Sign Up
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p className="mt-4 text-center text-sm text-gray-600">
|
<p className="mt-4 text-center text-xs sm:text-sm text-gray-600">
|
||||||
Already have an account?{' '}
|
Already have an account?{' '}
|
||||||
<Link to="/" className="font-semibold text-[#0033FF] hover:underline">
|
<Link to="/" className="font-semibold text-[#0033FF] hover:underline">
|
||||||
Sign in
|
Sign in
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user