From 36334119bd6beaa98a25a8c0ba2ab328f8e89708 Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Sun, 24 May 2026 13:07:07 +0330 Subject: [PATCH] chore(env): update application version to 2.2.4 in production environment feat(multi-select): enhance MultiSelect component with dropdown positioning and portal rendering - Added dynamic dropdown positioning to ensure it appears correctly relative to the trigger element. - Implemented portal rendering for the dropdown to improve performance and prevent overflow issues. - Refactored MultiSelect state management for better clarity and maintainability. feat(switch): improve Switch component with GSAP animations and enhanced styling - Integrated GSAP animations for thumb transitions and glow effects to enhance user interaction. - Updated styling for the Switch component to improve visual feedback and responsiveness. feat(companies-table): add currency and language metadata display in CompaniesTable - Introduced currency and language metadata for better representation of company details. - Enhanced UI for displaying company languages and currencies with tooltips for additional context. feat(assign-to-company-modal): refactor AssignToCompanyModalContent to use MultiSelect - Replaced Select component with MultiSelect for improved user experience in selecting companies. - Streamlined state management and data handling for company assignments. feat(customers-table): enhance customer company display with tooltips and improved layout - Updated customer company display to show a maximum of two companies with a tooltip for additional companies. - Improved styling for better visual representation of customer associations with companies. feat(confirmation-modal): enhance ConfirmationModal with animations and improved accessibility - Added GSAP animations for modal entrance and exit to improve user experience. - Implemented accessibility features for better keyboard navigation and focus management. feat(status): extend Status component with pulse and shimmer effects for enhanced visual feedback - Introduced new visual effects for status indicators to improve user engagement and clarity. - Updated styles for better integration with existing UI components. feat(tooltip): enhance tooltip styling and functionality with new palettes - Implemented a new tooltip design with gradient backgrounds and shadows for improved visibility. - Updated tooltip parameters to support new styling options and enhance user experience. --- .env.production | 2 +- src/assets/icons.tsx | 1 + src/components/FormElements/MultiSelect.tsx | 50 ++- src/components/FormElements/switch.tsx | 199 ++++++++++- src/components/Tables/companies/index.tsx | 82 ++++- .../customers/AssignToCompanyModalContent.tsx | 98 +++--- src/components/Tables/customers/index.tsx | 56 +++- src/components/ui/ConfirmationModal.tsx | 313 ++++++++++++++---- src/components/ui/Status.tsx | 80 ++++- src/constants/tooltip.ts | 76 ++++- src/css/style.css | 33 ++ src/lib/api/services/customers-service.ts | 2 +- src/lib/api/types/Customers.ts | 2 +- 13 files changed, 829 insertions(+), 165 deletions(-) diff --git a/.env.production b/.env.production index cc233eb..06f08b9 100644 --- a/.env.production +++ b/.env.production @@ -1,4 +1,4 @@ -NEXT_PUBLIC_APP_VERSION=2.2.3 +NEXT_PUBLIC_APP_VERSION=2.2.4 NEXT_PUBLIC_TOAST_AUTO_CLOSE_DURATION=2500 NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSyCTjdsk2jE34IfnvSKP1JMTIf_Abd7tbt0 NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=opplens-270d1.firebaseapp.com diff --git a/src/assets/icons.tsx b/src/assets/icons.tsx index 719ef43..2eb4a7f 100644 --- a/src/assets/icons.tsx +++ b/src/assets/icons.tsx @@ -75,6 +75,7 @@ export function MoneyIcon(props: IconProps) { height="20px" viewBox="0 -960 960 960" width="20px" + fill="currentColor" {...props} > diff --git a/src/components/FormElements/MultiSelect.tsx b/src/components/FormElements/MultiSelect.tsx index 8a0d7e9..ccf1e17 100644 --- a/src/components/FormElements/MultiSelect.tsx +++ b/src/components/FormElements/MultiSelect.tsx @@ -1,7 +1,15 @@ "use client"; import { cn } from "@/lib/utils"; -import React, { useCallback, useEffect, useId, useRef, useState } from "react"; +import React, { + useCallback, + useEffect, + useId, + useLayoutEffect, + useRef, + useState, +} from "react"; +import { createPortal } from "react-dom"; import { type FieldErrors, type FieldValues, @@ -59,11 +67,40 @@ function MultiSelect({ const [selectedItems, setSelectedItems] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [show, setShow] = useState(false); + const [dropdownStyle, setDropdownStyle] = useState({}); + const [portalReady, setPortalReady] = useState(false); const dropdownRef = useRef(null); const listRef = useRef(null); const trigger = useRef(null); const initializedRef = useRef(false); + useEffect(() => { + setPortalReady(typeof document !== "undefined"); + }, []); + + const updateDropdownPosition = useCallback(() => { + if (!trigger.current) return; + const rect = trigger.current.getBoundingClientRect(); + setDropdownStyle({ + position: "fixed", + top: rect.bottom + 4, + left: rect.left, + width: rect.width, + }); + }, []); + + useLayoutEffect(() => { + if (!show) return; + updateDropdownPosition(); + const handle = () => updateDropdownPosition(); + window.addEventListener("resize", handle); + window.addEventListener("scroll", handle, true); + return () => { + window.removeEventListener("resize", handle); + window.removeEventListener("scroll", handle, true); + }; + }, [show, updateDropdownPosition]); + // Initialize selectedItems from external value + items (edit mode) useEffect(() => { if (initializedRef.current) return; @@ -238,10 +275,13 @@ function MultiSelect({ + {portalReady && + show && + createPortal(
@@ -314,7 +354,9 @@ function MultiSelect({ )}
- + , + document.body, + )} {error && ( diff --git a/src/components/FormElements/switch.tsx b/src/components/FormElements/switch.tsx index c94f557..4ec5317 100644 --- a/src/components/FormElements/switch.tsx +++ b/src/components/FormElements/switch.tsx @@ -1,6 +1,14 @@ import { CheckIcon, XIcon } from "@/assets/icons"; import { cn } from "@/lib/utils"; -import { ChangeEvent, ToggleEvent, useId } from "react"; +import gsap from "gsap"; +import { + ChangeEvent, + ToggleEvent, + useEffect, + useId, + useLayoutEffect, + useRef, +} from "react"; type PropsType = { withIcon?: boolean; @@ -24,11 +32,147 @@ export function Switch({ onChange, }: PropsType) { const id = useId(); + const isSm = backgroundSize === "sm"; + const isDark = background === "dark"; + + const TRAVEL = isSm ? 28 : 24; + + const thumbRef = useRef(null); + const trackRef = useRef(null); + const glowRef = useRef(null); + const checkWrapRef = useRef(null); + const xWrapRef = useRef(null); + const initialized = useRef(false); + const activeTweens = useRef([]); + + useLayoutEffect(() => { + if (thumbRef.current) { + gsap.set(thumbRef.current, { x: checked ? TRAVEL : 0 }); + } + if (withIcon && checkWrapRef.current && xWrapRef.current) { + gsap.set(checkWrapRef.current, { + autoAlpha: checked ? 1 : 0, + rotateY: 0, + }); + gsap.set(xWrapRef.current, { + autoAlpha: checked ? 0 : 1, + rotateY: 0, + }); + } + if (glowRef.current) { + gsap.set(glowRef.current, { autoAlpha: 0, scale: 0.6 }); + } + initialized.current = true; + // run once on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (!initialized.current) return; + if (!thumbRef.current || !trackRef.current) return; + + // Kill in-flight tweens WITHOUT reverting — new tweens will start + // from wherever the element currently sits, so toggling mid-animation + // (and toggling ON→OFF after ON has settled) feels symmetric. + activeTweens.current.forEach((t) => t.kill()); + activeTweens.current = []; + + const tl = gsap.timeline(); + + tl.to(thumbRef.current, { + scaleY: 0.82, + scaleX: 1.18, + duration: 0.12, + ease: "power2.out", + }) + .to( + thumbRef.current, + { + x: checked ? TRAVEL : 0, + duration: 0.4, + ease: "back.out(2.2)", + }, + "<", + ) + .to( + thumbRef.current, + { + scaleY: 1, + scaleX: 1, + duration: 0.5, + ease: "elastic.out(1, 0.45)", + }, + "-=0.2", + ); + activeTweens.current.push(tl as unknown as gsap.core.Tween); + + activeTweens.current.push( + gsap.fromTo( + trackRef.current, + { scale: 1 }, + { + scale: 1.05, + duration: 0.14, + ease: "power2.out", + yoyo: true, + repeat: 1, + }, + ), + ); + + if (checked && glowRef.current) { + activeTweens.current.push( + gsap.fromTo( + glowRef.current, + { autoAlpha: 0.85, scale: 0.5 }, + { + autoAlpha: 0, + scale: 1.9, + duration: 0.7, + ease: "power2.out", + }, + ), + ); + } + + if (withIcon && checkWrapRef.current && xWrapRef.current) { + const show = checked ? checkWrapRef.current : xWrapRef.current; + const hide = checked ? xWrapRef.current : checkWrapRef.current; + activeTweens.current.push( + gsap.to(hide, { + rotateY: 90, + autoAlpha: 0, + duration: 0.18, + ease: "power2.in", + }), + ); + activeTweens.current.push( + gsap.fromTo( + show, + { rotateY: -90, autoAlpha: 0 }, + { + rotateY: 0, + autoAlpha: 1, + duration: 0.32, + ease: "back.out(2)", + delay: 0.16, + }, + ), + ); + } + }, [checked, TRAVEL, withIcon]); + + useEffect(() => { + return () => { + activeTweens.current.forEach((t) => t.kill()); + activeTweens.current = []; + }; + }, []); return (