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.
This commit is contained in:
+1
-1
@@ -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_TOAST_AUTO_CLOSE_DURATION=2500
|
||||||
NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSyCTjdsk2jE34IfnvSKP1JMTIf_Abd7tbt0
|
NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSyCTjdsk2jE34IfnvSKP1JMTIf_Abd7tbt0
|
||||||
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=opplens-270d1.firebaseapp.com
|
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=opplens-270d1.firebaseapp.com
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export function MoneyIcon(props: IconProps) {
|
|||||||
height="20px"
|
height="20px"
|
||||||
viewBox="0 -960 960 960"
|
viewBox="0 -960 960 960"
|
||||||
width="20px"
|
width="20px"
|
||||||
|
fill="currentColor"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<path d="M220-60 80-200l140-140 57 56-44 44h494l-43-44 56-56 140 140L740-60l-57-56 44-44H233l43 44-56 56Zm260-460q-50 0-85-35t-35-85q0-50 35-85t85-35q50 0 85 35t35 85q0 50-35 85t-85 35ZM200-400q-33 0-56.5-23.5T120-480v-320q0-33 23.5-56.5T200-880h560q33 0 56.5 23.5T840-800v320q0 33-23.5 56.5T760-400H200Zm80-80h400q0-33 23.5-56.5T760-560v-160q-33 0-56.5-23.5T680-800H280q0 33-23.5 56.5T200-720v160q33 0 56.5 23.5T280-480Zm-80 0v-320 320Z" />
|
<path d="M220-60 80-200l140-140 57 56-44 44h494l-43-44 56-56 140 140L740-60l-57-56 44-44H233l43 44-56 56Zm260-460q-50 0-85-35t-35-85q0-50 35-85t85-35q50 0 85 35t35 85q0 50-35 85t-85 35ZM200-400q-33 0-56.5-23.5T120-480v-320q0-33 23.5-56.5T200-880h560q33 0 56.5 23.5T840-800v320q0 33-23.5 56.5T760-400H200Zm80-80h400q0-33 23.5-56.5T760-560v-160q-33 0-56.5-23.5T680-800H280q0 33-23.5 56.5T200-720v160q33 0 56.5 23.5T280-480Zm-80 0v-320 320Z" />
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
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 {
|
import {
|
||||||
type FieldErrors,
|
type FieldErrors,
|
||||||
type FieldValues,
|
type FieldValues,
|
||||||
@@ -59,11 +67,40 @@ function MultiSelect<T extends FieldValues>({
|
|||||||
const [selectedItems, setSelectedItems] = useState<Option[]>([]);
|
const [selectedItems, setSelectedItems] = useState<Option[]>([]);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
|
const [dropdownStyle, setDropdownStyle] = useState<React.CSSProperties>({});
|
||||||
|
const [portalReady, setPortalReady] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const listRef = useRef<HTMLDivElement>(null);
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
const trigger = useRef<HTMLDivElement | null>(null);
|
const trigger = useRef<HTMLDivElement | null>(null);
|
||||||
const initializedRef = useRef(false);
|
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)
|
// Initialize selectedItems from external value + items (edit mode)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initializedRef.current) return;
|
if (initializedRef.current) return;
|
||||||
@@ -238,10 +275,13 @@ function MultiSelect<T extends FieldValues>({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{portalReady &&
|
||||||
|
show &&
|
||||||
|
createPortal(
|
||||||
<div
|
<div
|
||||||
|
style={dropdownStyle}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute left-0 top-full z-40 mt-1 w-full rounded-lg border border-stroke bg-white shadow-lg dark:border-dark-3 dark:bg-dark-2",
|
"z-[1000000] rounded-lg border border-stroke bg-white shadow-lg dark:border-dark-3 dark:bg-dark-2",
|
||||||
!show && "hidden",
|
|
||||||
)}
|
)}
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
>
|
>
|
||||||
@@ -314,7 +354,9 @@ function MultiSelect<T extends FieldValues>({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { CheckIcon, XIcon } from "@/assets/icons";
|
import { CheckIcon, XIcon } from "@/assets/icons";
|
||||||
import { cn } from "@/lib/utils";
|
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 = {
|
type PropsType = {
|
||||||
withIcon?: boolean;
|
withIcon?: boolean;
|
||||||
@@ -24,11 +32,147 @@ export function Switch({
|
|||||||
onChange,
|
onChange,
|
||||||
}: PropsType) {
|
}: PropsType) {
|
||||||
const id = useId();
|
const id = useId();
|
||||||
|
const isSm = backgroundSize === "sm";
|
||||||
|
const isDark = background === "dark";
|
||||||
|
|
||||||
|
const TRAVEL = isSm ? 28 : 24;
|
||||||
|
|
||||||
|
const thumbRef = useRef<HTMLDivElement>(null);
|
||||||
|
const trackRef = useRef<HTMLDivElement>(null);
|
||||||
|
const glowRef = useRef<HTMLDivElement>(null);
|
||||||
|
const checkWrapRef = useRef<HTMLSpanElement>(null);
|
||||||
|
const xWrapRef = useRef<HTMLSpanElement>(null);
|
||||||
|
const initialized = useRef(false);
|
||||||
|
const activeTweens = useRef<gsap.core.Tween[]>([]);
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<label
|
<label
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
className="flex max-w-fit cursor-pointer select-none items-center"
|
className="group/switch flex max-w-fit cursor-pointer select-none items-center"
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
@@ -41,27 +185,54 @@ export function Switch({
|
|||||||
checked={checked}
|
checked={checked}
|
||||||
data-cy={dataCy}
|
data-cy={dataCy}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Track */}
|
||||||
<div
|
<div
|
||||||
className={cn("h-8 w-14 rounded-full bg-gray-3 dark:bg-[#5A616B]", {
|
ref={trackRef}
|
||||||
"h-5": backgroundSize === "sm",
|
className={cn(
|
||||||
"bg-[#212B36] dark:bg-primary": background === "dark",
|
"h-8 w-14 origin-center rounded-full bg-gradient-to-br from-gray-3 to-gray-4 shadow-[inset_0_1px_2px_rgba(0,0,0,0.12)] transition-[background-image,box-shadow] duration-300 dark:from-[#5A616B] dark:to-[#3F4651]",
|
||||||
})}
|
"peer-checked:bg-gradient-to-br peer-checked:from-primary peer-checked:to-primary/80 peer-checked:shadow-[0_0_0_4px_rgba(91,93,185,0.18),inset_0_1px_2px_rgba(0,0,0,0.12)] peer-focus-visible:ring-2 peer-focus-visible:ring-primary/40",
|
||||||
|
"group-active/switch:scale-[0.97]",
|
||||||
|
isSm && "h-5",
|
||||||
|
isDark &&
|
||||||
|
"from-[#212B36] to-[#0F1620] dark:from-primary dark:to-primary/80",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Glow burst (only fires when turning ON) */}
|
||||||
<div
|
<div
|
||||||
|
ref={glowRef}
|
||||||
|
className="pointer-events-none absolute inset-0 rounded-full bg-primary opacity-0"
|
||||||
|
style={{ filter: "blur(10px)" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Thumb */}
|
||||||
|
<div
|
||||||
|
ref={thumbRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute left-1 top-1 flex size-6 items-center justify-center rounded-full bg-white shadow-switch-1 transition peer-checked:right-1 peer-checked:translate-x-full peer-checked:[&_.check-icon]:block peer-checked:[&_.x-icon]:hidden",
|
"pointer-events-none absolute left-1 top-1 flex size-6 items-center justify-center rounded-full bg-gradient-to-br from-white to-gray-2 shadow-switch-1 ring-1 ring-black/5",
|
||||||
{
|
"peer-checked:from-white peer-checked:to-white peer-checked:shadow-[0_2px_6px_rgba(0,0,0,0.18),0_0_0_1px_rgba(91,93,185,0.25)]",
|
||||||
"-top-1 left-0 size-7 shadow-switch-2": backgroundSize === "sm",
|
"group-active/switch:scale-95",
|
||||||
"peer-checked:bg-primary peer-checked:dark:bg-primary":
|
isSm && "-top-1 left-0 size-7 shadow-switch-2",
|
||||||
background !== "dark",
|
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
|
style={{ perspective: 400 }}
|
||||||
>
|
>
|
||||||
{withIcon && (
|
{withIcon && (
|
||||||
<>
|
<>
|
||||||
<CheckIcon className="check-icon hidden fill-white" />
|
<span
|
||||||
<XIcon className="x-icon" />
|
ref={checkWrapRef}
|
||||||
|
className="absolute inline-flex items-center justify-center"
|
||||||
|
style={{ transformStyle: "preserve-3d", backfaceVisibility: "hidden" }}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-3 w-3 fill-primary text-primary" />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
ref={xWrapRef}
|
||||||
|
className="absolute inline-flex items-center justify-center"
|
||||||
|
style={{ transformStyle: "preserve-3d", backfaceVisibility: "hidden" }}
|
||||||
|
>
|
||||||
|
<XIcon className="h-3 w-3 text-gray-5" />
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,30 @@ import { Tooltip } from "react-tooltip";
|
|||||||
import CompanyListFilters from "./CompanyListFilters";
|
import CompanyListFilters from "./CompanyListFilters";
|
||||||
import { useCompanyListPresenter } from "./useCompanyListPresenter";
|
import { useCompanyListPresenter } from "./useCompanyListPresenter";
|
||||||
|
|
||||||
|
const CURRENCY_META: Record<string, { symbol: string; label: string }> = {
|
||||||
|
USD: { symbol: "$", label: "US Dollar" },
|
||||||
|
EUR: { symbol: "€", label: "Euro" },
|
||||||
|
GBP: { symbol: "£", label: "British Pound" },
|
||||||
|
JPY: { symbol: "¥", label: "Japanese Yen" },
|
||||||
|
CAD: { symbol: "C$", label: "Canadian Dollar" },
|
||||||
|
AUD: { symbol: "A$", label: "Australian Dollar" },
|
||||||
|
CHF: { symbol: "₣", label: "Swiss Franc" },
|
||||||
|
CNY: { symbol: "¥", label: "Chinese Yuan" },
|
||||||
|
INR: { symbol: "₹", label: "Indian Rupee" },
|
||||||
|
BRL: { symbol: "R$", label: "Brazilian Real" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const LANGUAGE_META: Record<string, { flag: string; label: string }> = {
|
||||||
|
en: { flag: "🇺🇸", label: "English" },
|
||||||
|
ar: { flag: "🇦🇪", label: "Arabic" },
|
||||||
|
fr: { flag: "🇫🇷", label: "French" },
|
||||||
|
es: { flag: "🇪🇸", label: "Spanish" },
|
||||||
|
de: { flag: "🇩🇪", label: "German" },
|
||||||
|
zh: { flag: "🇨🇳", label: "Chinese" },
|
||||||
|
ja: { flag: "🇯🇵", label: "Japanese" },
|
||||||
|
ko: { flag: "🇰🇷", label: "Korean" },
|
||||||
|
};
|
||||||
|
|
||||||
const CompaniesTable = () => {
|
const CompaniesTable = () => {
|
||||||
const {
|
const {
|
||||||
isPending,
|
isPending,
|
||||||
@@ -121,10 +145,64 @@ const CompaniesTable = () => {
|
|||||||
{company.address.state}
|
{company.address.state}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className="text-start" colSpan={100}>
|
||||||
{company.language}
|
{(() => {
|
||||||
|
const code = company.language?.toLowerCase?.() ?? "";
|
||||||
|
const meta = LANGUAGE_META[code];
|
||||||
|
if (!meta) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full border border-dashed border-stroke px-2.5 py-1 text-xs font-medium text-dark-5 dark:border-dark-3 dark:text-dark-6">
|
||||||
|
—
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-tooltip-id={`lang-${company.id}`}
|
||||||
|
data-tooltip-content={meta.label}
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-full border border-indigo-500/20 bg-indigo-500/10 px-2.5 py-1 text-xs font-semibold uppercase tracking-wide text-indigo-600 transition-colors hover:bg-indigo-500/20 dark:border-indigo-400/30 dark:bg-indigo-400/15 dark:text-indigo-200"
|
||||||
|
>
|
||||||
|
<span className="text-sm leading-none">
|
||||||
|
{meta.flag}
|
||||||
|
</span>
|
||||||
|
{code}
|
||||||
|
<Tooltip
|
||||||
|
{..._TooltipDefaultParams({
|
||||||
|
id: `lang-${company.id}`,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className="text-start" colSpan={100}>
|
||||||
{company.currency}
|
{(() => {
|
||||||
|
const code = company.currency?.toUpperCase?.() ?? "";
|
||||||
|
const meta = CURRENCY_META[code];
|
||||||
|
if (!meta) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full border border-dashed border-stroke px-2.5 py-1 text-xs font-medium text-dark-5 dark:border-dark-3 dark:text-dark-6">
|
||||||
|
—
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-tooltip-id={`currency-${company.id}`}
|
||||||
|
data-tooltip-content={meta.label}
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-full border border-emerald-500/20 bg-emerald-500/10 px-2.5 py-1 text-xs font-semibold tracking-wide text-emerald-600 transition-colors hover:bg-emerald-500/20 dark:border-emerald-400/30 dark:bg-emerald-400/15 dark:text-emerald-200"
|
||||||
|
>
|
||||||
|
<span className="flex h-4 w-4 items-center justify-center rounded-full bg-emerald-500/20 text-[10px] font-bold leading-none dark:bg-emerald-400/25">
|
||||||
|
{meta.symbol}
|
||||||
|
</span>
|
||||||
|
{code}
|
||||||
|
<Tooltip
|
||||||
|
{..._TooltipDefaultParams({
|
||||||
|
id: `currency-${company.id}`,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className="text-start" colSpan={100}>
|
||||||
{company.employee_count}
|
{company.employee_count}
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
import {
|
import { Dispatch, FC, SetStateAction, useCallback, useMemo, useState } from "react";
|
||||||
ChangeEvent,
|
|
||||||
Dispatch,
|
|
||||||
FC,
|
|
||||||
SetStateAction,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
import { Select } from "@/components/FormElements/select";
|
import MultiSelect from "@/components/FormElements/MultiSelect";
|
||||||
import { Building } from "@/components/Layouts/sidebar/icons";
|
import {
|
||||||
import { ApiResponse, TAssignCustomerToCompanyCredentials, TCompaniesResponse } from "@/lib/api";
|
ApiResponse,
|
||||||
import { ILabelValue } from "@/types/shared";
|
TAssignCustomerToCompanyCredentials,
|
||||||
|
TCompaniesResponse,
|
||||||
|
} from "@/lib/api";
|
||||||
import { UseQueryResult } from "@tanstack/react-query";
|
import { UseQueryResult } from "@tanstack/react-query";
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
companiesQuery: UseQueryResult<ApiResponse<TCompaniesResponse>, Error>;
|
companiesQuery: UseQueryResult<ApiResponse<TCompaniesResponse>, Error>;
|
||||||
@@ -21,66 +15,52 @@ interface IProps {
|
|||||||
>;
|
>;
|
||||||
defaultValues?: TAssignCustomerToCompanyCredentials;
|
defaultValues?: TAssignCustomerToCompanyCredentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AssignToCompanyModalContent: FC<IProps> = ({
|
const AssignToCompanyModalContent: FC<IProps> = ({
|
||||||
companiesQuery,
|
companiesQuery,
|
||||||
setCompanies,
|
setCompanies,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
}) => {
|
}) => {
|
||||||
const [options, setOptions] = useState<ILabelValue[]>([]);
|
const { data, isSuccess } = companiesQuery;
|
||||||
|
const [selected, setSelected] = useState<string[]>(
|
||||||
|
defaultValues?.companies ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
const { data, status, isSuccess } = companiesQuery;
|
const options = useMemo(() => {
|
||||||
const { handleSubmit, watch, setValue } =
|
if (!isSuccess) return [];
|
||||||
useForm<TAssignCustomerToCompanyCredentials>({
|
return (
|
||||||
mode: "onChange",
|
data?.data.companies?.map((item) => ({
|
||||||
defaultValues,
|
label: item.name,
|
||||||
});
|
value: item.id,
|
||||||
|
})) ?? []
|
||||||
|
);
|
||||||
|
}, [isSuccess, data?.data.companies]);
|
||||||
|
|
||||||
const selectedCompanyId = watch("company_ids")?.[0] || "";
|
const handleChange = useCallback(
|
||||||
|
(event: { target: { value: string[] } }) => {
|
||||||
|
const next = event.target.value;
|
||||||
|
setSelected(next);
|
||||||
|
setCompanies({ companies: next });
|
||||||
|
},
|
||||||
|
[setCompanies],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const noop = useCallback(() => {}, []);
|
||||||
if (isSuccess) {
|
|
||||||
setOptions(
|
|
||||||
data.data.companies
|
|
||||||
? data.data.companies?.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.id,
|
|
||||||
}))
|
|
||||||
: [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [status, isSuccess, data?.data.companies]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (defaultValues?.company_ids?.[0]) {
|
|
||||||
setValue("company_ids", defaultValues.company_ids);
|
|
||||||
}
|
|
||||||
}, [defaultValues, setValue]);
|
|
||||||
|
|
||||||
const assignCompany = (data: TAssignCustomerToCompanyCredentials) => {
|
|
||||||
setCompanies(data);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<form className="mt-5" onSubmit={handleSubmit(assignCompany)}>
|
<div className="mt-5">
|
||||||
<Select
|
<MultiSelect
|
||||||
name="company_ids"
|
name="companies"
|
||||||
clearable
|
|
||||||
label="Companies"
|
label="Companies"
|
||||||
items={options}
|
items={options}
|
||||||
prefixIcon={<Building />}
|
value={selected}
|
||||||
placeholder="Please Select company"
|
placeholder="Please select companies"
|
||||||
value={selectedCompanyId}
|
|
||||||
onChange={
|
|
||||||
((e: ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setValue("company_ids", [value]);
|
|
||||||
setCompanies({
|
|
||||||
company_ids: [value],
|
|
||||||
});
|
|
||||||
}) as any
|
|
||||||
}
|
|
||||||
required
|
required
|
||||||
|
onChange={handleChange as any}
|
||||||
|
onBlur={noop as any}
|
||||||
|
ref={noop as any}
|
||||||
/>
|
/>
|
||||||
</form>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -120,11 +120,47 @@ const CustomersTable = () => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell colSpan={100}>
|
<TableCell colSpan={100}>
|
||||||
{customer?.companies?.length ? (
|
{customer?.companies?.length ? (
|
||||||
customer?.companies.map((c) => (
|
<div className="flex max-w-[260px] flex-wrap items-center gap-1.5">
|
||||||
<span key={c.id}>{c.name}, </span>
|
{customer.companies.slice(0, 2).map((c) => (
|
||||||
))
|
<span
|
||||||
|
key={c.id}
|
||||||
|
data-tooltip-id={`company-${customer.id}-${c.id}`}
|
||||||
|
data-tooltip-content={c.name}
|
||||||
|
className="inline-flex max-w-[140px] items-center gap-1.5 rounded-full border border-primary/20 bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary transition-colors hover:bg-primary/20 dark:border-primary/30 dark:bg-primary/20 dark:text-white"
|
||||||
|
>
|
||||||
|
<Building className="h-3 w-3 shrink-0" />
|
||||||
|
<span className="truncate">{c.name}</span>
|
||||||
|
<Tooltip
|
||||||
|
{..._TooltipDefaultParams({
|
||||||
|
id: `company-${customer.id}-${c.id}`,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{customer.companies.length > 2 && (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
data-tooltip-id={`company-more-${customer.id}`}
|
||||||
|
data-tooltip-content={customer.companies
|
||||||
|
.slice(2)
|
||||||
|
.map((c) => c.name)
|
||||||
|
.join(", ")}
|
||||||
|
className="inline-flex cursor-default items-center rounded-full border border-stroke bg-gray-2 px-2.5 py-1 text-xs font-semibold text-dark transition-colors hover:bg-gray-3 dark:border-dark-3 dark:bg-dark-3 dark:text-white"
|
||||||
|
>
|
||||||
|
+{customer.companies.length - 2}
|
||||||
|
</span>
|
||||||
|
<Tooltip
|
||||||
|
{..._TooltipDefaultParams({
|
||||||
|
id: `company-more-${customer.id}`,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span>None</span>
|
<span className="inline-flex items-center gap-1 rounded-full border border-dashed border-stroke px-2.5 py-1 text-xs font-medium text-dark-5 dark:border-dark-3 dark:text-dark-6">
|
||||||
|
None
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell colSpan={100} className="uppercase">
|
<TableCell colSpan={100} className="uppercase">
|
||||||
@@ -168,7 +204,8 @@ const CustomersTable = () => {
|
|||||||
setCurrentCustomer(customer);
|
setCurrentCustomer(customer);
|
||||||
setIsAssignCompanyModalOpen(true);
|
setIsAssignCompanyModalOpen(true);
|
||||||
setSelectedCompanies({
|
setSelectedCompanies({
|
||||||
company_ids: [customer?.companies?.[0]?.id ?? ""],
|
companies:
|
||||||
|
customer?.companies?.map((c) => c.id) ?? [],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -210,12 +247,15 @@ const CustomersTable = () => {
|
|||||||
<Modal
|
<Modal
|
||||||
classNames="w-full xl:w-1/2"
|
classNames="w-full xl:w-1/2"
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
if (!selectedCompanies || !currentCustomer) {
|
if (
|
||||||
toast.error("Invalid credentials");
|
!currentCustomer ||
|
||||||
|
!selectedCompanies?.companies?.length
|
||||||
|
) {
|
||||||
|
toast.error("Please select at least one company");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
assignSelectedCompanies({
|
assignSelectedCompanies({
|
||||||
credentials: selectedCompanies,
|
credentials: { companies: selectedCompanies.companies },
|
||||||
id: currentCustomer?.id ?? "",
|
id: currentCustomer?.id ?? "",
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import { CheckIcon, ExclamationIcon } from "@/assets/icons";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { useIsMutating } from "@tanstack/react-query";
|
import { useIsMutating } from "@tanstack/react-query";
|
||||||
import React, { useEffect, useState } from "react";
|
import gsap from "gsap";
|
||||||
|
import React, {
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -23,6 +31,64 @@ export type ConfirmationModalProps = z.infer<
|
|||||||
typeof ConfirmationModalPropsSchema
|
typeof ConfirmationModalPropsSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
type Variant = "default" | "danger" | "warning";
|
||||||
|
|
||||||
|
const variantStyles: Record<
|
||||||
|
Variant,
|
||||||
|
{
|
||||||
|
accent: string;
|
||||||
|
iconWrap: string;
|
||||||
|
iconGlow: string;
|
||||||
|
iconColor: string;
|
||||||
|
confirmBtn: string;
|
||||||
|
focusRing: string;
|
||||||
|
Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
default: {
|
||||||
|
accent:
|
||||||
|
"bg-gradient-to-r from-primary via-[#6B63F3] to-primary",
|
||||||
|
iconWrap:
|
||||||
|
"bg-gradient-to-br from-primary/15 via-primary/10 to-primary/5 ring-1 ring-inset ring-primary/20",
|
||||||
|
iconGlow: "bg-primary/30",
|
||||||
|
iconColor: "text-primary",
|
||||||
|
confirmBtn:
|
||||||
|
"bg-gradient-to-br from-primary to-[#4C46C9] hover:from-primary hover:to-primary text-white shadow-[0_6px_18px_-4px_rgba(87,80,241,0.55)]",
|
||||||
|
focusRing: "focus-visible:ring-primary/40",
|
||||||
|
Icon: CheckIcon,
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
accent:
|
||||||
|
"bg-gradient-to-r from-error via-error-light to-error",
|
||||||
|
iconWrap:
|
||||||
|
"bg-gradient-to-br from-error/15 via-error/10 to-error/5 ring-1 ring-inset ring-error/20",
|
||||||
|
iconGlow: "bg-error/35",
|
||||||
|
iconColor: "text-error",
|
||||||
|
confirmBtn:
|
||||||
|
"bg-gradient-to-br from-error to-error-dark hover:from-error hover:to-error text-white shadow-[0_6px_18px_-4px_rgba(242,48,48,0.55)]",
|
||||||
|
focusRing: "focus-visible:ring-error/40",
|
||||||
|
Icon: ExclamationIcon,
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
accent:
|
||||||
|
"bg-gradient-to-r from-yellow-dark via-yellow-light to-yellow-dark",
|
||||||
|
iconWrap:
|
||||||
|
"bg-gradient-to-br from-yellow-light/40 via-yellow-light/25 to-yellow-light/10 ring-1 ring-inset ring-yellow-dark/20",
|
||||||
|
iconGlow: "bg-yellow-dark/30",
|
||||||
|
iconColor: "text-yellow-dark-2",
|
||||||
|
confirmBtn:
|
||||||
|
"bg-gradient-to-br from-yellow-dark to-yellow-dark-2 hover:from-yellow-dark hover:to-yellow-dark text-white shadow-[0_6px_18px_-4px_rgba(217,119,6,0.5)]",
|
||||||
|
focusRing: "focus-visible:ring-yellow-dark/40",
|
||||||
|
Icon: ExclamationIcon,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeStyles: Record<"sm" | "md" | "lg", string> = {
|
||||||
|
sm: "max-w-sm",
|
||||||
|
md: "max-w-md",
|
||||||
|
lg: "max-w-lg",
|
||||||
|
};
|
||||||
|
|
||||||
const ConfirmationModal: React.FC<ConfirmationModalProps> = (props) => {
|
const ConfirmationModal: React.FC<ConfirmationModalProps> = (props) => {
|
||||||
const isMutating = useIsMutating();
|
const isMutating = useIsMutating();
|
||||||
const validatedProps = ConfirmationModalPropsSchema.parse(props);
|
const validatedProps = ConfirmationModalPropsSchema.parse(props);
|
||||||
@@ -39,12 +105,27 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = (props) => {
|
|||||||
} = validatedProps;
|
} = validatedProps;
|
||||||
|
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [shouldRender, setShouldRender] = useState(isOpen);
|
||||||
|
|
||||||
|
const overlayRef = useRef<HTMLDivElement>(null);
|
||||||
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
|
const iconRef = useRef<HTMLDivElement>(null);
|
||||||
|
const iconGlowRef = useRef<HTMLDivElement>(null);
|
||||||
|
const activeTween = useRef<gsap.core.Timeline | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
return () => setMounted(false);
|
return () => setMounted(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Mount/unmount gating so exit animation can play before unmount.
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setShouldRender(true);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Escape + body scroll lock
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleEscape = (event: KeyboardEvent) => {
|
const handleEscape = (event: KeyboardEvent) => {
|
||||||
if (event.key === "Escape" && isOpen) {
|
if (event.key === "Escape" && isOpen) {
|
||||||
@@ -63,100 +144,214 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = (props) => {
|
|||||||
};
|
};
|
||||||
}, [isOpen, onCancel]);
|
}, [isOpen, onCancel]);
|
||||||
|
|
||||||
if (!mounted || !isOpen) return null;
|
// Drive entrance/exit animations
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!shouldRender) return;
|
||||||
|
if (!overlayRef.current || !panelRef.current) return;
|
||||||
|
|
||||||
const variantStyles = {
|
activeTween.current?.kill();
|
||||||
default: {
|
|
||||||
confirmBtn: "bg-primary hover:bg-primary/90 focus:ring-primary",
|
|
||||||
icon: "🔔",
|
|
||||||
iconBg: "bg-blue-100",
|
|
||||||
},
|
|
||||||
danger: {
|
|
||||||
confirmBtn: "bg-red-light hover:bg-red-light/90 focus:ring-red-light",
|
|
||||||
icon: "⚠️",
|
|
||||||
iconBg: "bg-red-light/20",
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
confirmBtn:
|
|
||||||
"bg-yellow-dark hover:bg-yellow-dark/90 focus:ring-yellow-dark",
|
|
||||||
icon: "⚠️",
|
|
||||||
iconBg: "bg-yellow-light/20",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const sizeStyles = {
|
if (isOpen) {
|
||||||
sm: "max-w-sm",
|
gsap.set(overlayRef.current, { autoAlpha: 0 });
|
||||||
md: "max-w-md",
|
gsap.set(panelRef.current, {
|
||||||
lg: "max-w-lg",
|
autoAlpha: 0,
|
||||||
};
|
scale: 0.9,
|
||||||
|
y: 24,
|
||||||
|
});
|
||||||
|
if (iconRef.current) {
|
||||||
|
gsap.set(iconRef.current, { scale: 0, rotation: -90 });
|
||||||
|
}
|
||||||
|
if (iconGlowRef.current) {
|
||||||
|
gsap.set(iconGlowRef.current, { autoAlpha: 0, scale: 0.4 });
|
||||||
|
}
|
||||||
|
|
||||||
const currentVariant = variantStyles[variant];
|
const tl = gsap.timeline();
|
||||||
|
tl.to(overlayRef.current, {
|
||||||
|
autoAlpha: 1,
|
||||||
|
duration: 0.28,
|
||||||
|
ease: "power2.out",
|
||||||
|
})
|
||||||
|
.to(
|
||||||
|
panelRef.current,
|
||||||
|
{
|
||||||
|
autoAlpha: 1,
|
||||||
|
scale: 1,
|
||||||
|
y: 0,
|
||||||
|
duration: 0.55,
|
||||||
|
ease: "back.out(1.5)",
|
||||||
|
},
|
||||||
|
"-=0.18",
|
||||||
|
)
|
||||||
|
.to(
|
||||||
|
iconRef.current,
|
||||||
|
{
|
||||||
|
scale: 1,
|
||||||
|
rotation: 0,
|
||||||
|
duration: 0.55,
|
||||||
|
ease: "back.out(2.4)",
|
||||||
|
},
|
||||||
|
"-=0.32",
|
||||||
|
)
|
||||||
|
.fromTo(
|
||||||
|
iconGlowRef.current,
|
||||||
|
{ autoAlpha: 0.7, scale: 0.5 },
|
||||||
|
{
|
||||||
|
autoAlpha: 0,
|
||||||
|
scale: 1.8,
|
||||||
|
duration: 0.9,
|
||||||
|
ease: "power2.out",
|
||||||
|
},
|
||||||
|
"-=0.4",
|
||||||
|
);
|
||||||
|
activeTween.current = tl;
|
||||||
|
} else {
|
||||||
|
const tl = gsap.timeline({
|
||||||
|
onComplete: () => setShouldRender(false),
|
||||||
|
});
|
||||||
|
tl.to(panelRef.current, {
|
||||||
|
autoAlpha: 0,
|
||||||
|
scale: 0.94,
|
||||||
|
y: 12,
|
||||||
|
duration: 0.22,
|
||||||
|
ease: "power2.in",
|
||||||
|
}).to(
|
||||||
|
overlayRef.current,
|
||||||
|
{
|
||||||
|
autoAlpha: 0,
|
||||||
|
duration: 0.22,
|
||||||
|
ease: "power2.in",
|
||||||
|
},
|
||||||
|
"-=0.18",
|
||||||
|
);
|
||||||
|
activeTween.current = tl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
activeTween.current?.kill();
|
||||||
|
};
|
||||||
|
}, [isOpen, shouldRender]);
|
||||||
|
|
||||||
|
if (!mounted || !shouldRender) return null;
|
||||||
|
|
||||||
|
const v = variantStyles[variant];
|
||||||
|
const VariantIcon = v.Icon;
|
||||||
const currentSize = sizeStyles[size];
|
const currentSize = sizeStyles[size];
|
||||||
|
const isBusy = isMutating > 0;
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 overflow-y-auto"
|
className="fixed inset-0 z-[999999] overflow-y-auto"
|
||||||
aria-labelledby="modal-title"
|
aria-labelledby="modal-title"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
data-cy="confirmation-modal"
|
data-cy="confirmation-modal"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-dark/70 bg-opacity-75 transition-opacity"
|
ref={overlayRef}
|
||||||
onClick={onCancel}
|
aria-hidden
|
||||||
|
className="pointer-events-none fixed inset-0 bg-gradient-to-br from-dark/80 via-dark/70 to-black/85 backdrop-blur-md"
|
||||||
data-cy="confirmation-modal-overlay"
|
data-cy="confirmation-modal-overlay"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
<div
|
||||||
|
className="relative flex min-h-full items-end justify-center p-4 sm:items-center sm:p-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) onCancel();
|
||||||
|
}}
|
||||||
|
data-cy="confirmation-modal-outside"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full ${currentSize} dark:bg-dark-2`}
|
ref={panelRef}
|
||||||
|
className={cn(
|
||||||
|
"relative w-full overflow-hidden rounded-2xl bg-white text-left shadow-2xl ring-1 ring-stroke/40 sm:my-8 dark:bg-dark-2 dark:ring-stroke-dark/60",
|
||||||
|
currentSize,
|
||||||
|
)}
|
||||||
|
style={{ willChange: "transform, opacity" }}
|
||||||
>
|
>
|
||||||
<div className="bg-white px-4 pb-4 pt-5 dark:bg-dark-2 sm:p-6 sm:pb-4">
|
{/* Top accent strip */}
|
||||||
<div className="sm:flex sm:items-start">
|
<div className={cn("h-1 w-full", v.accent)} />
|
||||||
<div
|
|
||||||
className={`mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full ${currentVariant.iconBg} sm:mx-0 sm:h-10 sm:w-10`}
|
{/* Soft decorative glow blob */}
|
||||||
>
|
<div
|
||||||
<span className="text-xl">{currentVariant.icon}</span>
|
aria-hidden
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none absolute -right-16 -top-16 h-40 w-40 rounded-full opacity-20 blur-3xl",
|
||||||
|
v.iconGlow,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="px-6 pb-5 pt-7 sm:px-7">
|
||||||
|
<div className="flex flex-col items-center text-center sm:flex-row sm:items-start sm:gap-5 sm:text-left">
|
||||||
|
{/* Icon with glow burst */}
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
ref={iconGlowRef}
|
||||||
|
aria-hidden
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none absolute inset-0 rounded-full blur-md",
|
||||||
|
v.iconGlow,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
ref={iconRef}
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-14 w-14 shrink-0 items-center justify-center rounded-full shadow-inner",
|
||||||
|
v.iconWrap,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<VariantIcon
|
||||||
|
className={cn("h-6 w-6", v.iconColor)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
<div className="mt-4 sm:mt-1">
|
||||||
<h3
|
<h3
|
||||||
className="text-base font-semibold leading-6 text-dark dark:text-white"
|
|
||||||
id="modal-title"
|
id="modal-title"
|
||||||
|
className="text-lg font-semibold leading-7 tracking-tight text-dark dark:text-white"
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="mt-2">
|
<p className="mt-1.5 text-sm leading-relaxed text-dark-5 dark:text-dark-6">
|
||||||
<p className="text-sm text-dark-5 dark:text-dark-6">
|
{message}
|
||||||
{message}
|
</p>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray px-4 py-3 dark:border-t dark:border-stroke-dark dark:bg-dark-2 sm:flex sm:flex-row-reverse sm:px-6">
|
{/* Divider */}
|
||||||
|
<div className="h-px w-full bg-gradient-to-r from-transparent via-stroke to-transparent dark:via-stroke-dark" />
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex flex-col-reverse gap-2.5 bg-gray-1/60 px-6 py-4 backdrop-blur-sm dark:bg-dark/30 sm:flex-row sm:justify-end sm:gap-3 sm:px-7">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:ml-3 sm:w-auto ${currentVariant.confirmBtn} dark:ring-offset-dark-2`}
|
className="inline-flex w-full items-center justify-center rounded-lg bg-white px-4 py-2.5 text-sm font-semibold text-dark shadow-sm ring-1 ring-inset ring-stroke transition-all duration-200 hover:-translate-y-0.5 hover:bg-gray-1 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-dark-5/40 active:translate-y-0 dark:bg-dark-3 dark:text-white dark:ring-stroke-dark dark:hover:bg-dark-4 sm:w-auto"
|
||||||
disabled={isMutating > 0}
|
|
||||||
onClick={onConfirm}
|
|
||||||
data-cy="confirmation-modal-confirm"
|
|
||||||
>
|
|
||||||
{isMutating ? (
|
|
||||||
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
|
|
||||||
) : (
|
|
||||||
confirmText
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-dark shadow-sm ring-1 ring-inset ring-stroke hover:bg-gray focus:outline-none focus:ring-2 focus:ring-dark-5 focus:ring-offset-2 dark:bg-dark-3 dark:text-white dark:ring-stroke-dark dark:hover:bg-dark-4 dark:focus:ring-offset-dark-2 sm:mt-0 sm:w-auto"
|
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
data-cy="confirmation-modal-cancel"
|
data-cy="confirmation-modal-cancel"
|
||||||
>
|
>
|
||||||
{cancelText}
|
{cancelText}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={onConfirm}
|
||||||
|
data-cy="confirmation-modal-confirm"
|
||||||
|
className={cn(
|
||||||
|
"group inline-flex w-full items-center justify-center gap-2 overflow-hidden rounded-lg px-4 py-2.5 text-sm font-semibold transition-all duration-200 hover:-translate-y-0.5 hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 active:translate-y-0 disabled:cursor-not-allowed disabled:opacity-70 disabled:hover:translate-y-0 dark:focus-visible:ring-offset-dark-2 sm:w-auto",
|
||||||
|
v.confirmBtn,
|
||||||
|
v.focusRing,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isBusy ? (
|
||||||
|
<span className="relative inline-flex items-center gap-2">
|
||||||
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/40 border-t-white" />
|
||||||
|
Processing…
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="relative">{confirmText}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ type Tone =
|
|||||||
| "urgent"
|
| "urgent"
|
||||||
| "featured";
|
| "featured";
|
||||||
|
|
||||||
|
type PulseKind = "ping" | "pulse" | "none";
|
||||||
|
|
||||||
function resolveTone(status: string): Tone {
|
function resolveTone(status: string): Tone {
|
||||||
if (greens.includes(status)) return "green";
|
if (greens.includes(status)) return "green";
|
||||||
if (reds.includes(status)) return "red";
|
if (reds.includes(status)) return "red";
|
||||||
@@ -56,80 +58,140 @@ function resolveTone(status: string): Tone {
|
|||||||
|
|
||||||
const toneStyles: Record<
|
const toneStyles: Record<
|
||||||
Tone,
|
Tone,
|
||||||
{ root: string; dot: string; dotRing?: string }
|
{
|
||||||
|
root: string;
|
||||||
|
dot: string;
|
||||||
|
dotRing?: string;
|
||||||
|
pingColor?: string;
|
||||||
|
pulse: PulseKind;
|
||||||
|
shimmer: boolean;
|
||||||
|
}
|
||||||
> = {
|
> = {
|
||||||
green: {
|
green: {
|
||||||
root: "bg-gradient-to-br from-green via-green to-green-dark text-white shadow-[0_2px_10px_-2px_rgba(34,173,92,0.55)] ring-1 ring-inset ring-white/25",
|
root: "bg-gradient-to-br from-green via-green to-green-dark text-white shadow-[0_2px_10px_-2px_rgba(34,173,92,0.55)] ring-1 ring-inset ring-white/25",
|
||||||
dot: "bg-white shadow-[0_0_10px_rgba(255,255,255,0.85)]",
|
dot: "bg-white shadow-[0_0_10px_rgba(255,255,255,0.85)]",
|
||||||
dotRing: "ring-1 ring-white/40",
|
dotRing: "ring-1 ring-white/40",
|
||||||
|
pingColor: "bg-white/70",
|
||||||
|
pulse: "ping",
|
||||||
|
shimmer: true,
|
||||||
},
|
},
|
||||||
red: {
|
red: {
|
||||||
root: "bg-gradient-to-br from-error via-error to-error-dark text-white shadow-[0_2px_10px_-2px_rgba(242,48,48,0.5)] ring-1 ring-inset ring-white/20",
|
root: "bg-gradient-to-br from-error via-error to-error-dark text-white shadow-[0_2px_10px_-2px_rgba(242,48,48,0.5)] ring-1 ring-inset ring-white/20",
|
||||||
dot: "bg-white shadow-[0_0_10px_rgba(255,255,255,0.75)]",
|
dot: "bg-white shadow-[0_0_10px_rgba(255,255,255,0.75)]",
|
||||||
dotRing: "ring-1 ring-white/35",
|
dotRing: "ring-1 ring-white/35",
|
||||||
|
pingColor: "bg-white/70",
|
||||||
|
pulse: "pulse",
|
||||||
|
shimmer: true,
|
||||||
},
|
},
|
||||||
blue: {
|
blue: {
|
||||||
root: "bg-gradient-to-br from-blue via-blue to-blue-dark text-white shadow-[0_2px_10px_-2px_rgba(60,80,224,0.45)] ring-1 ring-inset ring-white/25",
|
root: "bg-gradient-to-br from-blue via-blue to-blue-dark text-white shadow-[0_2px_10px_-2px_rgba(60,80,224,0.45)] ring-1 ring-inset ring-white/25",
|
||||||
dot: "bg-white shadow-[0_0_10px_rgba(255,255,255,0.8)]",
|
dot: "bg-white shadow-[0_0_10px_rgba(255,255,255,0.8)]",
|
||||||
dotRing: "ring-1 ring-white/35",
|
dotRing: "ring-1 ring-white/35",
|
||||||
|
pingColor: "bg-white/70",
|
||||||
|
pulse: "ping",
|
||||||
|
shimmer: true,
|
||||||
},
|
},
|
||||||
yellow: {
|
yellow: {
|
||||||
root: "border border-yellow-dark-2/15 bg-gradient-to-br from-yellow-light to-yellow-light/70 text-yellow-dark-2 shadow-sm shadow-yellow-dark/10 ring-1 ring-inset ring-white/60",
|
root: "border border-yellow-dark-2/15 bg-gradient-to-br from-yellow-light to-yellow-light/70 text-yellow-dark-2 shadow-sm shadow-yellow-dark/10 ring-1 ring-inset ring-white/60",
|
||||||
dot: "bg-yellow-dark-2 shadow-[0_0_8px_rgba(217,119,6,0.45)]",
|
dot: "bg-yellow-dark-2 shadow-[0_0_8px_rgba(217,119,6,0.45)]",
|
||||||
dotRing: "ring-2 ring-yellow-dark-2/25",
|
dotRing: "ring-2 ring-yellow-dark-2/25",
|
||||||
|
pingColor: "bg-yellow-dark-2/50",
|
||||||
|
pulse: "pulse",
|
||||||
|
shimmer: false,
|
||||||
},
|
},
|
||||||
orange: {
|
orange: {
|
||||||
root: "bg-gradient-to-br from-orange-light to-orange-light/85 text-white shadow-[0_2px_10px_-2px_rgba(245,148,96,0.45)] ring-1 ring-inset ring-white/25",
|
root: "bg-gradient-to-br from-orange-light to-orange-light/85 text-white shadow-[0_2px_10px_-2px_rgba(245,148,96,0.45)] ring-1 ring-inset ring-white/25",
|
||||||
dot: "bg-white shadow-[0_0_8px_rgba(255,255,255,0.7)]",
|
dot: "bg-white shadow-[0_0_8px_rgba(255,255,255,0.7)]",
|
||||||
dotRing: "ring-1 ring-white/30",
|
dotRing: "ring-1 ring-white/30",
|
||||||
|
pingColor: "bg-white/70",
|
||||||
|
pulse: "pulse",
|
||||||
|
shimmer: true,
|
||||||
},
|
},
|
||||||
gray: {
|
gray: {
|
||||||
root: "border border-gray-4/60 bg-gradient-to-b from-gray-1 to-gray-2 text-gray-7 shadow-sm ring-1 ring-inset ring-white/80",
|
root: "border border-gray-4/60 bg-gradient-to-b from-gray-1 to-gray-2 text-gray-7 shadow-sm ring-1 ring-inset ring-white/80",
|
||||||
dot: "bg-gray-5 shadow-inner",
|
dot: "bg-gray-5 shadow-inner",
|
||||||
dotRing: "ring-1 ring-gray-4/50",
|
dotRing: "ring-1 ring-gray-4/50",
|
||||||
|
pulse: "none",
|
||||||
|
shimmer: false,
|
||||||
},
|
},
|
||||||
new: {
|
new: {
|
||||||
root: "border border-green-light-3/80 bg-gradient-to-br from-green-light-6 via-green-light-6 to-green-light-5 text-green-dark shadow-sm shadow-green/10 ring-1 ring-inset ring-white/70",
|
root: "border border-green-light-3/80 bg-gradient-to-br from-green-light-6 via-green-light-6 to-green-light-5 text-green-dark shadow-sm shadow-green/10 ring-1 ring-inset ring-white/70",
|
||||||
dot: "bg-green shadow-[0_0_8px_rgba(34,173,92,0.5)]",
|
dot: "bg-green shadow-[0_0_8px_rgba(34,173,92,0.5)]",
|
||||||
dotRing: "ring-2 ring-green/20",
|
dotRing: "ring-2 ring-green/20",
|
||||||
|
pingColor: "bg-green/60",
|
||||||
|
pulse: "ping",
|
||||||
|
shimmer: false,
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
root: "border border-blue-light-3/80 bg-gradient-to-br from-blue-light-5 via-blue-light-5 to-blue-light-4 text-blue-dark shadow-sm shadow-blue/10 ring-1 ring-inset ring-white/70",
|
root: "border border-blue-light-3/80 bg-gradient-to-br from-blue-light-5 via-blue-light-5 to-blue-light-4 text-blue-dark shadow-sm shadow-blue/10 ring-1 ring-inset ring-white/70",
|
||||||
dot: "bg-blue shadow-[0_0_8px_rgba(60,80,224,0.45)]",
|
dot: "bg-blue shadow-[0_0_8px_rgba(60,80,224,0.45)]",
|
||||||
dotRing: "ring-2 ring-blue/15",
|
dotRing: "ring-2 ring-blue/15",
|
||||||
|
pingColor: "bg-blue/60",
|
||||||
|
pulse: "ping",
|
||||||
|
shimmer: false,
|
||||||
},
|
},
|
||||||
urgent: {
|
urgent: {
|
||||||
root: "border border-error-light-3/90 bg-gradient-to-br from-error-light-6 via-error-light-6 to-error-light-5 text-error-dark shadow-sm shadow-error/10 ring-1 ring-inset ring-white/70",
|
root: "border border-error-light-3/90 bg-gradient-to-br from-error-light-6 via-error-light-6 to-error-light-5 text-error-dark shadow-sm shadow-error/10 ring-1 ring-inset ring-white/70",
|
||||||
dot: "bg-error shadow-[0_0_8px_rgba(242,48,48,0.45)]",
|
dot: "bg-error shadow-[0_0_8px_rgba(242,48,48,0.45)]",
|
||||||
dotRing: "ring-2 ring-error/20",
|
dotRing: "ring-2 ring-error/20",
|
||||||
|
pingColor: "bg-error/60",
|
||||||
|
pulse: "pulse",
|
||||||
|
shimmer: false,
|
||||||
},
|
},
|
||||||
featured: {
|
featured: {
|
||||||
root: "bg-gradient-to-br from-primary via-[#6B63F3] to-primary text-white shadow-[0_2px_12px_-2px_rgba(87,80,241,0.55)] ring-1 ring-inset ring-white/30",
|
root: "bg-gradient-to-br from-primary via-[#6B63F3] to-primary text-white shadow-[0_2px_12px_-2px_rgba(87,80,241,0.55)] ring-1 ring-inset ring-white/30",
|
||||||
dot: "bg-white shadow-[0_0_12px_rgba(255,255,255,0.9)]",
|
dot: "bg-white shadow-[0_0_12px_rgba(255,255,255,0.9)]",
|
||||||
dotRing: "ring-1 ring-white/40",
|
dotRing: "ring-1 ring-white/40",
|
||||||
|
pingColor: "bg-white/70",
|
||||||
|
pulse: "ping",
|
||||||
|
shimmer: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Status = ({ status = "draft", children }: IProps) => {
|
const Status = ({ status = "draft", children }: IProps) => {
|
||||||
const tone = resolveTone(status);
|
const tone = resolveTone(status);
|
||||||
const { root, dot, dotRing } = toneStyles[tone];
|
const { root, dot, dotRing, pingColor, pulse, shimmer } = toneStyles[tone];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex max-w-full items-center justify-center gap-2 rounded-full px-3.5 py-1.5 text-center text-xs font-semibold capitalize tracking-wide",
|
"group/status relative inline-flex max-w-full items-center justify-center gap-2 overflow-hidden rounded-full px-3.5 py-1.5 text-center text-xs font-semibold capitalize tracking-wide transition-all duration-300 ease-out",
|
||||||
|
"hover:-translate-y-[1px] hover:brightness-110 motion-reduce:hover:translate-y-0",
|
||||||
root,
|
root,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{/* Shimmer sweep */}
|
||||||
|
{shimmer && (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute inset-y-0 -left-1/3 w-1/3 -skew-x-12 bg-gradient-to-r from-transparent via-white/30 to-transparent motion-safe:animate-[shimmer_3.2s_ease-in-out_infinite] motion-reduce:hidden"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dot + radar ping */}
|
||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className={cn(
|
className="relative inline-flex h-2 w-2 shrink-0 items-center justify-center"
|
||||||
"relative inline-flex h-2 w-2 shrink-0 rounded-full",
|
>
|
||||||
dot,
|
{pulse === "ping" && pingColor && (
|
||||||
dotRing,
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute inline-flex h-full w-full rounded-full opacity-75 motion-safe:animate-ping motion-reduce:hidden",
|
||||||
|
pingColor,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
<span
|
||||||
<span className="min-w-0 truncate">{children}</span>
|
className={cn(
|
||||||
|
"relative inline-flex h-2 w-2 rounded-full",
|
||||||
|
dot,
|
||||||
|
dotRing,
|
||||||
|
pulse === "pulse" && "motion-safe:animate-pulse",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="relative min-w-0 truncate">{children}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,58 @@
|
|||||||
import { CSSProperties } from "react";
|
import { CSSProperties } from "react";
|
||||||
import { VariantType } from "react-tooltip";
|
import { VariantType } from "react-tooltip";
|
||||||
|
|
||||||
|
type Palette = {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
shadow: string;
|
||||||
|
border: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const variantPalettes: Record<string, Palette> = {
|
||||||
|
error: {
|
||||||
|
from: "#F23030",
|
||||||
|
to: "#B00B0B",
|
||||||
|
shadow:
|
||||||
|
"0 10px 30px -10px rgba(242,48,48,0.55), inset 0 1px 0 rgba(255,255,255,0.18)",
|
||||||
|
border: "rgba(255,255,255,0.14)",
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
from: "#5750F1",
|
||||||
|
to: "#3B36B3",
|
||||||
|
shadow:
|
||||||
|
"0 10px 30px -10px rgba(87,80,241,0.55), inset 0 1px 0 rgba(255,255,255,0.2)",
|
||||||
|
border: "rgba(255,255,255,0.16)",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
from: "#22AD5C",
|
||||||
|
to: "#147F3D",
|
||||||
|
shadow:
|
||||||
|
"0 10px 30px -10px rgba(34,173,92,0.55), inset 0 1px 0 rgba(255,255,255,0.2)",
|
||||||
|
border: "rgba(255,255,255,0.16)",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
from: "#FBB040",
|
||||||
|
to: "#D97706",
|
||||||
|
shadow:
|
||||||
|
"0 10px 30px -10px rgba(217,119,6,0.55), inset 0 1px 0 rgba(255,255,255,0.2)",
|
||||||
|
border: "rgba(255,255,255,0.16)",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
from: "#1F2937",
|
||||||
|
to: "#0B1220",
|
||||||
|
shadow:
|
||||||
|
"0 10px 30px -10px rgba(0,0,0,0.65), inset 0 1px 0 rgba(255,255,255,0.08)",
|
||||||
|
border: "rgba(255,255,255,0.08)",
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
from: "#FFFFFF",
|
||||||
|
to: "#F1F5F9",
|
||||||
|
shadow:
|
||||||
|
"0 10px 30px -10px rgba(0,0,0,0.18), inset 0 1px 0 rgba(255,255,255,0.6)",
|
||||||
|
border: "rgba(15,23,42,0.06)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const _TooltipDefaultParams = ({
|
export const _TooltipDefaultParams = ({
|
||||||
id,
|
id,
|
||||||
variant = id === "delete" ? "error" : "info",
|
variant = id === "delete" ? "error" : "info",
|
||||||
@@ -9,10 +61,20 @@ export const _TooltipDefaultParams = ({
|
|||||||
id: string;
|
id: string;
|
||||||
variant?: VariantType;
|
variant?: VariantType;
|
||||||
styles?: CSSProperties;
|
styles?: CSSProperties;
|
||||||
}) => ({
|
}) => {
|
||||||
id,
|
const palette = variantPalettes[variant] ?? variantPalettes.info;
|
||||||
delayShow: 500,
|
|
||||||
delayHide: 500,
|
return {
|
||||||
variant,
|
id,
|
||||||
style: { backgroundColor: variant === "error" ? "#F23030" : "", ...styles },
|
delayShow: 220,
|
||||||
});
|
delayHide: 120,
|
||||||
|
variant,
|
||||||
|
className: "fancy-tooltip",
|
||||||
|
style: {
|
||||||
|
background: `linear-gradient(135deg, ${palette.from} 0%, ${palette.to} 100%)`,
|
||||||
|
boxShadow: palette.shadow,
|
||||||
|
border: `1px solid ${palette.border}`,
|
||||||
|
...styles,
|
||||||
|
} satisfies CSSProperties,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -164,6 +164,39 @@
|
|||||||
@apply !rounded-full !transition-opacity !duration-500;
|
@apply !rounded-full !transition-opacity !duration-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Fancy tooltip ---- */
|
||||||
|
.react-tooltip.fancy-tooltip {
|
||||||
|
@apply !rounded-md !px-2 !py-0.5 !font-medium !tracking-normal !text-white;
|
||||||
|
font-size: 10.5px !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
z-index: 99999;
|
||||||
|
transform: scale(0.85) translateY(4px);
|
||||||
|
transform-origin: center;
|
||||||
|
transition:
|
||||||
|
opacity var(--rt-transition-show-delay, 0.15s) ease-out,
|
||||||
|
transform 260ms cubic-bezier(0.34, 1.56, 0.64, 1) !important;
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tooltip.fancy-tooltip.react-tooltip__show {
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tooltip.fancy-tooltip.react-tooltip__closing {
|
||||||
|
transform: scale(0.92) translateY(2px);
|
||||||
|
transition:
|
||||||
|
opacity var(--rt-transition-closing-delay, 0.15s) ease-in,
|
||||||
|
transform 200ms cubic-bezier(0.4, 0, 1, 1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tooltip.fancy-tooltip .react-tooltip-arrow {
|
||||||
|
background: inherit;
|
||||||
|
border: inherit;
|
||||||
|
width: 6px !important;
|
||||||
|
height: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.tableCheckbox:checked ~ div {
|
.tableCheckbox:checked ~ div {
|
||||||
@apply border-primary bg-primary;
|
@apply border-primary bg-primary;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const customersService = {
|
|||||||
}): Promise<any> => {
|
}): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
await api.post(
|
await api.patch(
|
||||||
API_ENDPOINTS.CUSTOMERS.ASSIGN_COMPANY_TO_CUSTOMER(id),
|
API_ENDPOINTS.CUSTOMERS.ASSIGN_COMPANY_TO_CUSTOMER(id),
|
||||||
credentials,
|
credentials,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export const customersListResponse = z.object({
|
|||||||
customers: z.array(customersSchema),
|
customers: z.array(customersSchema),
|
||||||
});
|
});
|
||||||
const assignCustomerToCompanySchema = z.object({
|
const assignCustomerToCompanySchema = z.object({
|
||||||
company_ids: z.array(z.string()),
|
companies: z.array(z.string()).min(1),
|
||||||
});
|
});
|
||||||
export const CustomerListResponseSchema = createApiResponseSchema(
|
export const CustomerListResponseSchema = createApiResponseSchema(
|
||||||
customersListResponse,
|
customersListResponse,
|
||||||
|
|||||||
Reference in New Issue
Block a user