From 81f60acd06a9feeee297a7f4f9b3b6c17b14439c Mon Sep 17 00:00:00 2001 From: Rodolfo Date: Wed, 3 Dec 2025 01:20:30 -0500 Subject: [PATCH] additional GUI work --- .../UIX/AuthPageHeader/AuthPageHeader.jsx | 105 +++++++++ .../src/components/UIX/Input/Input.jsx | 128 +++++++---- .../src/components/UIX/Logo/Logo.jsx | 104 +++++++++ .../src/components/UIX/NavBar/NavBar.jsx | 80 +++++++ .../src/components/UIX/index.jsx | 9 + .../src/pages/Anonymous/Login/RequestOTT.jsx | 52 ++--- .../Anonymous/Recovery/InitiateRecovery.jsx | 199 +++++------------- .../src/pages/Anonymous/Register/Register.jsx | 75 ++----- 8 files changed, 467 insertions(+), 285 deletions(-) create mode 100644 web/maplefile-frontend/src/components/UIX/AuthPageHeader/AuthPageHeader.jsx create mode 100644 web/maplefile-frontend/src/components/UIX/Logo/Logo.jsx create mode 100644 web/maplefile-frontend/src/components/UIX/NavBar/NavBar.jsx diff --git a/web/maplefile-frontend/src/components/UIX/AuthPageHeader/AuthPageHeader.jsx b/web/maplefile-frontend/src/components/UIX/AuthPageHeader/AuthPageHeader.jsx new file mode 100644 index 0000000..0a32324 --- /dev/null +++ b/web/maplefile-frontend/src/components/UIX/AuthPageHeader/AuthPageHeader.jsx @@ -0,0 +1,105 @@ +// File: src/components/UIX/AuthPageHeader/AuthPageHeader.jsx +// AuthPageHeader Component - Centered icon with title for auth pages + +import React, { memo, useMemo } from "react"; +import { useUIXTheme } from "../themes/useUIXTheme.jsx"; + +/** + * AuthPageHeader Component - Performance Optimized + * Displays a centered icon with title for authentication page headers + * + * @param {string} title - The page title + * @param {React.Component} icon - Icon component to display + * @param {string} size - Size variant: 'sm', 'md', 'lg' (default: 'md') + * @param {string} className - Additional CSS classes + * @param {boolean} animate - Whether to animate on load (default: true) + * @param {boolean} showGlow - Whether to show glow effect (default: true) + */ + +const SIZE_CONFIG = Object.freeze({ + sm: { + container: "h-12 w-12", + icon: "h-6 w-6", + title: "text-2xl", + radius: "rounded-xl", + glowInset: "-inset-0.5", + }, + md: { + container: "h-16 w-16", + icon: "h-8 w-8", + title: "text-3xl", + radius: "rounded-2xl", + glowInset: "-inset-1", + }, + lg: { + container: "h-20 w-20", + icon: "h-10 w-10", + title: "text-4xl", + radius: "rounded-3xl", + glowInset: "-inset-1.5", + }, +}); + +const AuthPageHeader = memo(function AuthPageHeader({ + title, + icon: Icon, + size = "md", + className = "", + animate = true, + showGlow = true, +}) { + const { getThemeClasses } = useUIXTheme(); + + const sizeConfig = SIZE_CONFIG[size] || SIZE_CONFIG.md; + + const themeClasses = useMemo( + () => ({ + bgGradient: getThemeClasses("bg-gradient-secondary"), + textPrimary: getThemeClasses("text-primary"), + }), + [getThemeClasses] + ); + + const containerClasses = useMemo( + () => + `text-center ${animate ? "animate-fade-in-up" : ""} ${className}`.trim(), + [animate, className] + ); + + const iconContainerClasses = useMemo( + () => + `flex items-center justify-center ${sizeConfig.container} ${themeClasses.bgGradient} ${sizeConfig.radius} shadow-lg`, + [sizeConfig, themeClasses.bgGradient] + ); + + const glowClasses = useMemo( + () => + `absolute ${sizeConfig.glowInset} ${themeClasses.bgGradient} ${sizeConfig.radius} blur opacity-20`, + [sizeConfig, themeClasses.bgGradient] + ); + + const titleClasses = useMemo( + () => `${sizeConfig.title} font-bold ${themeClasses.textPrimary}`, + [sizeConfig.title, themeClasses.textPrimary] + ); + + return ( +
+ {Icon && ( +
+
+
+ +
+ {showGlow &&
} +
+
+ )} + {title &&

{title}

} +
+ ); +}); + +AuthPageHeader.displayName = "AuthPageHeader"; + +export default AuthPageHeader; diff --git a/web/maplefile-frontend/src/components/UIX/Input/Input.jsx b/web/maplefile-frontend/src/components/UIX/Input/Input.jsx index 9b0ff21..c42b0f8 100644 --- a/web/maplefile-frontend/src/components/UIX/Input/Input.jsx +++ b/web/maplefile-frontend/src/components/UIX/Input/Input.jsx @@ -1,11 +1,25 @@ // File: src/components/UIX/Input/Input.jsx +// Input Component - Performance Optimized -import React, { useMemo } from "react"; +import React, { useMemo, useCallback, memo } from "react"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { useUIXTheme } from "../themes/useUIXTheme.jsx"; +// Static size configurations - moved outside component to prevent recreation +const SIZE_CLASSES = Object.freeze({ + sm: "px-3 py-2 text-sm", + md: "px-4 py-3 text-base", + lg: "px-5 py-4 text-base sm:text-lg", +}); + +const LABEL_SIZE_CLASSES = Object.freeze({ + sm: "text-sm", + md: "text-base", + lg: "text-base sm:text-lg", +}); + /** - * Input Component + * Input Component - Performance Optimized * Text input field with label, validation, and icon support * * @param {string} label - Input label @@ -30,7 +44,7 @@ import { useUIXTheme } from "../themes/useUIXTheme.jsx"; * Correct usage: onChange={(value) => setMyValue(value)} * Incorrect usage: onChange={(e) => setMyValue(e.target.value)} // This will cause errors! */ -function Input({ +const Input = memo(function Input({ label, type = "text", placeholder, @@ -65,33 +79,51 @@ function Input({ ); }, [props.id, props.name]); - const sizeClasses = { - sm: "px-3 py-2 text-sm", - md: "px-4 py-3 text-base", - lg: "px-5 py-4 text-base sm:text-lg", - }; + // Memoize theme classes to prevent multiple getThemeClasses calls on each render + const themeClasses = useMemo( + () => ({ + textPrimary: getThemeClasses("text-primary"), + textDanger: getThemeClasses("text-danger"), + textMuted: getThemeClasses("text-muted"), + textSecondary: getThemeClasses("text-secondary"), + inputBorder: getThemeClasses("input-border"), + inputBorderError: getThemeClasses("input-border-error"), + inputFocusRing: getThemeClasses("input-focus-ring"), + bgDisabled: getThemeClasses("bg-disabled"), + }), + [getThemeClasses] + ); - const labelSizeClasses = { - sm: "text-sm", - md: "text-base", - lg: "text-base sm:text-lg", - }; + // Memoize the onChange handler to prevent recreation on each render + const handleChange = useCallback( + (e) => { + if (onChange) { + onChange(e.target.value); + } + }, + [onChange] + ); + // Memoize computed values const currentLength = value ? value.length : 0; const warningThreshold = characterCountWarning || characterWarningThreshold; const isNearLimit = maxLength && currentLength > (maxLength * warningThreshold) / 100; + // Memoize size class lookups + const sizeClass = SIZE_CLASSES[size] || SIZE_CLASSES.lg; + const labelSizeClass = LABEL_SIZE_CLASSES[size] || LABEL_SIZE_CLASSES.lg; + return (
{label && ( )} @@ -102,14 +134,14 @@ function Input({ flex items-center w-full border-2 rounded-xl shadow-sm transition-all duration-200 - ${error ? getThemeClasses("input-border-error") : getThemeClasses("input-border")} - ${disabled ? `${getThemeClasses("bg-disabled")} cursor-not-allowed` : "bg-white"} - focus-within:outline-none focus-within:${getThemeClasses("input-focus-ring")} + ${error ? themeClasses.inputBorderError : themeClasses.inputBorder} + ${disabled ? `${themeClasses.bgDisabled} cursor-not-allowed` : "bg-white"} + focus-within:outline-none focus-within:${themeClasses.inputFocusRing} `}> {prefix} @@ -120,14 +152,14 @@ function Input({ type={type} placeholder={placeholder} value={value} - onChange={(e) => onChange(e.target.value)} + onChange={handleChange} disabled={disabled} required={required} readOnly={readOnly} maxLength={maxLength} className={` flex-1 - ${sizeClasses[size]} + ${sizeClass} pl-0 bg-transparent border-0 @@ -142,7 +174,7 @@ function Input({ <> {Icon && (
- +
)} onChange(e.target.value)} + onChange={handleChange} disabled={disabled} required={required} readOnly={readOnly} maxLength={maxLength} className={` w-full - ${sizeClasses[size]} + ${sizeClass} ${Icon ? "pl-10" : "pl-5"} border-2 rounded-xl shadow-sm transition-all duration-200 placeholder-gray-400 - focus:outline-none ${getThemeClasses("input-focus-ring")} ${getThemeClasses("input-border")} - ${ - error - ? getThemeClasses("input-border-error") - : getThemeClasses("input-border") - } - ${disabled ? `${getThemeClasses("bg-disabled")} cursor-not-allowed` : "bg-white"} + focus:outline-none ${themeClasses.inputFocusRing} ${themeClasses.inputBorder} + ${error ? themeClasses.inputBorderError : themeClasses.inputBorder} + ${disabled ? `${themeClasses.bgDisabled} cursor-not-allowed` : "bg-white"} `} {...props} /> @@ -182,7 +210,7 @@ function Input({
{currentLength}/{maxLength} characters @@ -192,7 +220,7 @@ function Input({ {/* Helper text */} {helperText && !error && ( -

+

{helperText}

)} @@ -200,16 +228,42 @@ function Input({ {/* Error message */} {error && (

{error}

)}
); -} +}, +// Custom comparison function for memo - only re-render when these props change +(prevProps, nextProps) => { + return ( + prevProps.value === nextProps.value && + prevProps.error === nextProps.error && + prevProps.disabled === nextProps.disabled && + prevProps.label === nextProps.label && + prevProps.placeholder === nextProps.placeholder && + prevProps.type === nextProps.type && + prevProps.required === nextProps.required && + prevProps.readOnly === nextProps.readOnly && + prevProps.helperText === nextProps.helperText && + prevProps.maxLength === nextProps.maxLength && + prevProps.showCharacterCount === nextProps.showCharacterCount && + prevProps.size === nextProps.size && + prevProps.className === nextProps.className && + prevProps.prefix === nextProps.prefix && + prevProps.icon === nextProps.icon && + prevProps.onChange === nextProps.onChange && + prevProps.id === nextProps.id && + prevProps.name === nextProps.name + ); +}); + +// Set display name for React DevTools +Input.displayName = "Input"; export default Input; diff --git a/web/maplefile-frontend/src/components/UIX/Logo/Logo.jsx b/web/maplefile-frontend/src/components/UIX/Logo/Logo.jsx new file mode 100644 index 0000000..1de7187 --- /dev/null +++ b/web/maplefile-frontend/src/components/UIX/Logo/Logo.jsx @@ -0,0 +1,104 @@ +// File: src/components/UIX/Logo/Logo.jsx +// Logo Component - MapleFile branding with icon + +import React, { memo, useMemo } from "react"; +import { Link } from "react-router"; +import { LockClosedIcon } from "@heroicons/react/24/outline"; +import { useUIXTheme } from "../themes/useUIXTheme.jsx"; + +/** + * Logo Component - Performance Optimized + * Displays the MapleFile logo with icon and optional link + * + * @param {string} to - Link destination (default: "/") + * @param {string} size - Logo size: 'sm', 'md', 'lg' (default: 'md') + * @param {boolean} showText - Whether to show the text (default: true) + * @param {string} className - Additional CSS classes + * @param {React.Component} icon - Custom icon component (default: LockClosedIcon) + */ + +const SIZE_CONFIG = Object.freeze({ + sm: { + container: "h-8 w-8", + icon: "h-4 w-4", + text: "text-lg", + iconRadius: "rounded-lg", + }, + md: { + container: "h-10 w-10", + icon: "h-5 w-5", + text: "text-xl", + iconRadius: "rounded-xl", + }, + lg: { + container: "h-12 w-12", + icon: "h-6 w-6", + text: "text-2xl", + iconRadius: "rounded-xl", + }, +}); + +const Logo = memo(function Logo({ + to = "/", + size = "md", + showText = true, + className = "", + icon: Icon = LockClosedIcon, +}) { + const { getThemeClasses } = useUIXTheme(); + + const sizeConfig = SIZE_CONFIG[size] || SIZE_CONFIG.md; + + const themeClasses = useMemo( + () => ({ + bgGradient: getThemeClasses("bg-gradient-secondary"), + textPrimary: getThemeClasses("text-primary"), + }), + [getThemeClasses] + ); + + const containerClasses = useMemo( + () => `inline-flex items-center space-x-3 group ${className}`.trim(), + [className] + ); + + const iconContainerClasses = useMemo( + () => + `relative flex items-center justify-center ${sizeConfig.container} ${themeClasses.bgGradient} ${sizeConfig.iconRadius} shadow-md group-hover:shadow-lg transform group-hover:scale-105 transition-all duration-200`, + [sizeConfig, themeClasses.bgGradient] + ); + + const textClasses = useMemo( + () => + `${sizeConfig.text} font-bold ${themeClasses.textPrimary} transition-colors duration-200`, + [sizeConfig.text, themeClasses.textPrimary] + ); + + const content = ( + <> +
+
+
+ +
+
+ {showText && MapleFile} + + ); + + if (to) { + return ( + + {content} + + ); + } + + return
{content}
; +}); + +Logo.displayName = "Logo"; + +export default Logo; diff --git a/web/maplefile-frontend/src/components/UIX/NavBar/NavBar.jsx b/web/maplefile-frontend/src/components/UIX/NavBar/NavBar.jsx new file mode 100644 index 0000000..8148a12 --- /dev/null +++ b/web/maplefile-frontend/src/components/UIX/NavBar/NavBar.jsx @@ -0,0 +1,80 @@ +// File: src/components/UIX/NavBar/NavBar.jsx +// NavBar Component - Navigation bar for authentication pages + +import React, { memo, useMemo } from "react"; +import { Link } from "react-router"; +import { useUIXTheme } from "../themes/useUIXTheme.jsx"; +import Logo from "../Logo/Logo.jsx"; + +/** + * NavBar Component - Performance Optimized + * Navigation bar with logo and optional nav links + * + * @param {Array} links - Array of link objects { to, label, variant } + * @param {string} logoSize - Logo size: 'sm', 'md', 'lg' (default: 'md') + * @param {string} className - Additional CSS classes + * @param {boolean} transparent - Use transparent background (default: false) + * @param {React.ReactNode} children - Custom content to render on the right side + */ + +const NavBar = memo(function NavBar({ + links = [], + logoSize = "md", + className = "", + transparent = false, + children, +}) { + const { getThemeClasses } = useUIXTheme(); + + const themeClasses = useMemo( + () => ({ + bgCard: getThemeClasses("bg-card"), + borderSecondary: getThemeClasses("border-secondary"), + linkSecondary: getThemeClasses("link-secondary"), + }), + [getThemeClasses] + ); + + const navClasses = useMemo(() => { + const baseClasses = transparent + ? "backdrop-blur-sm" + : `${themeClasses.bgCard}/95 backdrop-blur-sm border-b ${themeClasses.borderSecondary}`; + return `${baseClasses} ${className}`.trim(); + }, [transparent, themeClasses, className]); + + const linkClasses = useMemo( + () => + `text-base font-medium ${themeClasses.linkSecondary} transition-colors duration-200`, + [themeClasses.linkSecondary] + ); + + const linksSection = useMemo(() => { + if (children) return children; + if (links.length === 0) return null; + + return ( +
+ {links.map((link, index) => ( + + {link.label} + + ))} +
+ ); + }, [links, linkClasses, children]); + + return ( + + ); +}); + +NavBar.displayName = "NavBar"; + +export default NavBar; diff --git a/web/maplefile-frontend/src/components/UIX/index.jsx b/web/maplefile-frontend/src/components/UIX/index.jsx index 3ab92f4..a6632c8 100644 --- a/web/maplefile-frontend/src/components/UIX/index.jsx +++ b/web/maplefile-frontend/src/components/UIX/index.jsx @@ -14,6 +14,9 @@ export { default as ActionCard } from "./ActionCard/ActionCard"; export { default as DeleteActionCard } from "./ActionCard/DeleteActionCard"; +// AuthPageHeader Component (New - centered icon with title for auth pages) +export { default as AuthPageHeader } from "./AuthPageHeader/AuthPageHeader"; + // Alert Components (Enhanced with modern styling) export { default as Alert, Notification, Message } from "./Alert/Alert"; @@ -175,6 +178,9 @@ export { export { default as LoadingOverlay } from "./Loading/LoadingOverlay"; export { default as Spinner, LoadingSpinner } from "./Loading/Spinner"; +// Logo Component (New - MapleFile branding with icon) +export { default as Logo } from "./Logo/Logo"; + // Modal Component export { default as Modal } from "./Modal/Modal"; @@ -184,6 +190,9 @@ export { default as MultiSelect } from "./MultiSelect/MultiSelect"; // Navigation Component (New - for reusable navigation bars) export { default as Navigation } from "./Navigation/Navigation"; +// NavBar Component (New - navigation bar for authentication pages) +export { default as NavBar } from "./NavBar/NavBar"; + // PageContainer Component (New - for full-page layouts with decorative elements) export { default as PageContainer } from "./PageContainer/PageContainer"; diff --git a/web/maplefile-frontend/src/pages/Anonymous/Login/RequestOTT.jsx b/web/maplefile-frontend/src/pages/Anonymous/Login/RequestOTT.jsx index 627581e..c2ff07a 100644 --- a/web/maplefile-frontend/src/pages/Anonymous/Login/RequestOTT.jsx +++ b/web/maplefile-frontend/src/pages/Anonymous/Login/RequestOTT.jsx @@ -3,11 +3,10 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { useNavigate, Link } from "react-router"; import { useServices } from "../../../services/Services"; -import { Card, Input, Button, Alert, GDPRFooter, ProgressIndicator, Divider } from "../../../components/UIX"; +import { Card, Input, Button, Alert, GDPRFooter, ProgressIndicator, Divider, NavBar, AuthPageHeader } from "../../../components/UIX"; import { useUIXTheme } from "../../../components/UIX/themes/useUIXTheme"; import { ArrowRightIcon, - LockClosedIcon, ShieldCheckIcon, SparklesIcon, } from "@heroicons/react/24/outline"; @@ -172,33 +171,13 @@ const RequestOTT = () => { {/* Header */}
- +
{/* Main Content */} @@ -216,17 +195,10 @@ const RequestOTT = () => { /> {/* Welcome Message */} -
-
-
-
-
- -
-
-
-

Secure Sign In

-
+ {/* Login Form */} { const navigate = useNavigate(); const { recoveryManager } = useServices(); + const { getThemeClasses } = useUIXTheme(); const [email, setEmail] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); @@ -98,78 +91,34 @@ const InitiateRecovery = () => { }, [navigate]); return ( -
+
{/* Navigation */} - + {/* Main Content */}
{/* Progress Indicator */} -
-
-
-
- 1 -
- - Email - -
-
-
-
- 2 -
- Verify -
-
-
-
- 3 -
- Reset -
-
-
+ {/* Header */} -
-
-
-
- -
-
-
-
-

- Account Recovery -

-
+ {/* Important Notice */} - +

Before You Begin @@ -183,11 +132,11 @@ const InitiateRecovery = () => { {/* Form Card */} - -
+ +
{/* Error Message */} {error && ( - +

Recovery Error @@ -199,29 +148,19 @@ const InitiateRecovery = () => { {/* Form */}
-
- - -

- We'll use this to verify your identity and guide you through - recovery -

-
+ + + {/* GDPR Notice */} + +
+

Data Processing Notice

+

+ Your email is temporarily stored in your browser to verify your identity—see our{" "} + + Privacy Policy + {" "} + for full details. +

+
+
{/* Back to Login Button */} @@ -262,52 +214,7 @@ const InitiateRecovery = () => {

{/* Footer */} - - - {/* CSS Animations */} - +
); }; diff --git a/web/maplefile-frontend/src/pages/Anonymous/Register/Register.jsx b/web/maplefile-frontend/src/pages/Anonymous/Register/Register.jsx index 9a069f8..5290df4 100644 --- a/web/maplefile-frontend/src/pages/Anonymous/Register/Register.jsx +++ b/web/maplefile-frontend/src/pages/Anonymous/Register/Register.jsx @@ -3,15 +3,13 @@ import { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { useNavigate, Link } from "react-router"; import { useServices } from "../../../services/Services"; -import { Card, Input, Select, Checkbox, Button, Alert, GDPRFooter } from "../../../components/UIX"; +import { Card, Input, Select, Checkbox, Button, Alert, GDPRFooter, NavBar, AuthPageHeader } from "../../../components/UIX"; import { useUIXTheme } from "../../../components/UIX/themes/useUIXTheme"; import { ArrowRightIcon, LockClosedIcon, ShieldCheckIcon, CheckIcon, - ExclamationTriangleIcon, - InformationCircleIcon, UserIcon, EnvelopeIcon, PhoneIcon, @@ -287,45 +285,19 @@ const Register = () => { return (
{/* Navigation */} - + {/* Main Content */}
{/* Header */} -
-
-
-
- -
-
-
-
-

- Create Your Secure Account -

+ +

Join MapleFile and protect your files with end-to-end encryption

@@ -521,7 +493,7 @@ const Register = () => { disabled={loading} /> {errors.agree_terms_of_service && ( -

+

{errors.agree_terms_of_service}

)} @@ -534,7 +506,7 @@ const Register = () => { onChange={(checked) => handleInputChange("agree_promotions", checked)} disabled={loading} /> -

+

We'll use your email address to send you important updates. You can unsubscribe anytime. Data retention: Until you unsubscribe or close your account.

@@ -552,7 +524,7 @@ const Register = () => { } disabled={loading} /> -

+

We collect anonymized usage data (page views, feature usage) to improve our service. No personal files or content are tracked. Data retention: 24 months. You can opt-out anytime in settings.

@@ -629,7 +601,7 @@ const Register = () => { Your files will be encrypted before leaving your device -
+

Data We Collect & Why (GDPR Transparency):

  • • Name & Email: Account management, authentication, communication (Legal basis: Contract)
  • @@ -658,27 +630,6 @@ const Register = () => { {/* Footer */} - -
); };