chore(env): update application version to 2.2.2 in production environment
feat(checkbox): enhance Checkbox component with animation and new icon options - Added GSAP animations for checkbox state changes to improve user interaction. - Introduced a new "none" icon option for the Checkbox component. - Updated Checkbox component to support additional props and improved styling. - Refactored Checkbox rendering logic for better performance and maintainability. feat(tender-filters): add document state filter to TenderListFilters - Integrated a new checkbox filter for "Documents scraped" in the TenderListFilters component. - Enhanced the UI for the filter section to improve user experience.
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
NEXT_PUBLIC_APP_VERSION=2.2.1
|
NEXT_PUBLIC_APP_VERSION=2.2.2
|
||||||
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
|
||||||
|
|||||||
@@ -113,9 +113,9 @@ export default function FormElementsPage() {
|
|||||||
title="Checkbox and radio"
|
title="Checkbox and radio"
|
||||||
className="space-y-5.5 !p-6.5"
|
className="space-y-5.5 !p-6.5"
|
||||||
>
|
>
|
||||||
{/* <Checkbox label="Checkbox Text" />
|
<Checkbox label="Checkbox Text" withIcon="check" withBg />
|
||||||
<Checkbox label="Checkbox Text" withIcon="check" />
|
<Checkbox label="With check icon" withIcon="check" withBg />
|
||||||
<Checkbox label="Checkbox Text" withIcon="x" /> */}
|
<Checkbox label="With X icon" withIcon="x" withBg />
|
||||||
<RadioInput label="Checkbox Text" />
|
<RadioInput label="Checkbox Text" />
|
||||||
<RadioInput label="Checkbox Text" variant="circle" />
|
<RadioInput label="Checkbox Text" variant="circle" />
|
||||||
</ShowcaseSection>
|
</ShowcaseSection>
|
||||||
|
|||||||
@@ -1,69 +1,238 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { CheckIcon, XIcon } from "@/assets/icons";
|
import { CheckIcon, XIcon } from "@/assets/icons";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useId } from "react";
|
import gsap from "gsap";
|
||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useId,
|
||||||
|
useImperativeHandle,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
type InputHTMLAttributes,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
withIcon?: "check" | "x";
|
withIcon?: "check" | "x" | "none";
|
||||||
withBg?: boolean;
|
withBg?: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
minimal?: boolean;
|
minimal?: boolean;
|
||||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
radius?: "default" | "md";
|
radius?: "default" | "md";
|
||||||
};
|
className?: string;
|
||||||
|
} & Omit<InputHTMLAttributes<HTMLInputElement>, "type">;
|
||||||
export function Checkbox({
|
|
||||||
withIcon,
|
|
||||||
label,
|
|
||||||
name,
|
|
||||||
withBg,
|
|
||||||
minimal,
|
|
||||||
onChange,
|
|
||||||
radius,
|
|
||||||
}: PropsType) {
|
|
||||||
const id = useId();
|
|
||||||
|
|
||||||
|
function prefersReducedMotion() {
|
||||||
return (
|
return (
|
||||||
<div>
|
typeof window !== "undefined" &&
|
||||||
<label
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||||
htmlFor={id}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-pointer select-none items-center",
|
|
||||||
!minimal && "text-body-sm font-medium",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
onChange={onChange}
|
|
||||||
name={name}
|
|
||||||
id={id}
|
|
||||||
className="peer sr-only"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"mr-2 flex size-5 items-center justify-center rounded border border-dark-5 peer-checked:border-primary dark:border-dark-6 peer-checked:[&>*]:block transition-all duration-300",
|
|
||||||
withBg
|
|
||||||
? "peer-checked:bg-primary [&>*]:text-white"
|
|
||||||
: "peer-checked:bg-gray-2 dark:peer-checked:bg-transparent",
|
|
||||||
minimal && "mr-3 border-stroke dark:border-dark-3",
|
|
||||||
radius === "md" && "rounded-md",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{!withIcon && (
|
|
||||||
<span className="hidden size-2.5 rounded-sm bg-primary" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{withIcon === "check" && (
|
|
||||||
<CheckIcon className="hidden text-primary" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{withIcon === "x" && <XIcon className="hidden text-primary" />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span>{label}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Checkbox = forwardRef<HTMLInputElement, PropsType>(function Checkbox(
|
||||||
|
{
|
||||||
|
withIcon = "check",
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
withBg = true,
|
||||||
|
minimal,
|
||||||
|
onChange,
|
||||||
|
radius = "md",
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
id: idProp,
|
||||||
|
checked,
|
||||||
|
defaultChecked,
|
||||||
|
...rest
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) {
|
||||||
|
const autoId = useId();
|
||||||
|
const id = idProp ?? autoId;
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const boxRef = useRef<HTMLDivElement>(null);
|
||||||
|
const fillRef = useRef<HTMLDivElement>(null);
|
||||||
|
const iconRef = useRef<HTMLSpanElement>(null);
|
||||||
|
const glowRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);
|
||||||
|
|
||||||
|
const animateState = (isChecked: boolean) => {
|
||||||
|
const box = boxRef.current;
|
||||||
|
if (!box || prefersReducedMotion()) return;
|
||||||
|
|
||||||
|
const fill = fillRef.current;
|
||||||
|
const icon = iconRef.current;
|
||||||
|
const glow = glowRef.current;
|
||||||
|
|
||||||
|
gsap.killTweensOf([box, fill, icon, glow].filter(Boolean));
|
||||||
|
|
||||||
|
if (isChecked) {
|
||||||
|
const tl = gsap.timeline();
|
||||||
|
tl.to(box, { scale: 1.14, duration: 0.1, ease: "power2.out" }).to(box, {
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.4,
|
||||||
|
ease: "back.out(2.6)",
|
||||||
|
});
|
||||||
|
if (fill) {
|
||||||
|
tl.fromTo(
|
||||||
|
fill,
|
||||||
|
{ scale: 0.35, opacity: 0 },
|
||||||
|
{ scale: 1, opacity: 1, duration: 0.32, ease: "power3.out" },
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (glow) {
|
||||||
|
tl.fromTo(
|
||||||
|
glow,
|
||||||
|
{ opacity: 0, scale: 0.75 },
|
||||||
|
{ opacity: 1, scale: 1, duration: 0.38, ease: "power2.out" },
|
||||||
|
0.04,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (icon) {
|
||||||
|
tl.fromTo(
|
||||||
|
icon,
|
||||||
|
{ scale: 0, rotate: -48, opacity: 0 },
|
||||||
|
{
|
||||||
|
scale: 1,
|
||||||
|
rotate: 0,
|
||||||
|
opacity: 1,
|
||||||
|
duration: 0.38,
|
||||||
|
ease: "back.out(3)",
|
||||||
|
},
|
||||||
|
0.1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gsap.to(box, { scale: 1, duration: 0.22, ease: "power2.out" });
|
||||||
|
if (fill) {
|
||||||
|
gsap.to(fill, {
|
||||||
|
scale: 0.4,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.2,
|
||||||
|
ease: "power2.in",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (glow) {
|
||||||
|
gsap.to(glow, { opacity: 0, scale: 0.85, duration: 0.2 });
|
||||||
|
}
|
||||||
|
if (icon) {
|
||||||
|
gsap.to(icon, {
|
||||||
|
scale: 0,
|
||||||
|
rotate: 36,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.18,
|
||||||
|
ease: "power2.in",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
animateState(inputRef.current.checked);
|
||||||
|
}
|
||||||
|
}, [checked]);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
animateState(e.target.checked);
|
||||||
|
onChange?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showCheckIcon = withIcon === "check";
|
||||||
|
const showXIcon = withIcon === "x";
|
||||||
|
const showDot = withIcon === "none";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className={cn(
|
||||||
|
"group flex cursor-pointer select-none items-center",
|
||||||
|
disabled && "cursor-not-allowed opacity-55",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="relative shrink-0">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="checkbox"
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
disabled={disabled}
|
||||||
|
checked={checked}
|
||||||
|
defaultChecked={defaultChecked}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="peer sr-only"
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={boxRef}
|
||||||
|
className={cn(
|
||||||
|
"relative mr-2.5 flex size-5 items-center justify-center overflow-hidden border-2 shadow-theme-xs backdrop-blur-sm transition-[border-color,box-shadow] duration-200",
|
||||||
|
"border-stroke/80 bg-white/90 dark:border-dark-3 dark:bg-dark-2/90",
|
||||||
|
"peer-checked:border-primary peer-checked:shadow-[0_0_0_3px_rgba(87,80,241,0.18)]",
|
||||||
|
"peer-focus-visible:ring-2 peer-focus-visible:ring-primary/45 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:peer-focus-visible:ring-offset-gray-dark",
|
||||||
|
"group-hover:border-primary/45 dark:group-hover:border-primary/55",
|
||||||
|
radius === "md" ? "rounded-lg" : "rounded",
|
||||||
|
minimal && "mr-3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={glowRef}
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute inset-0 opacity-0"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"radial-gradient(circle at 28% 22%, rgba(87,80,241,0.5), transparent 68%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(withBg || showCheckIcon || showXIcon) && (
|
||||||
|
<div
|
||||||
|
ref={fillRef}
|
||||||
|
aria-hidden
|
||||||
|
className="absolute inset-0 origin-center scale-[0.35] bg-gradient-to-br from-primary via-primary/90 to-[#7B6CFF] opacity-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDot && (
|
||||||
|
<span
|
||||||
|
ref={iconRef}
|
||||||
|
className="relative z-[1] block size-2.5 scale-0 rounded-sm bg-primary opacity-0 peer-checked:scale-100 peer-checked:opacity-100"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showCheckIcon && (
|
||||||
|
<span
|
||||||
|
ref={iconRef}
|
||||||
|
className="relative z-[1] inline-flex scale-0 opacity-0"
|
||||||
|
>
|
||||||
|
<CheckIcon className="size-3 text-white drop-shadow-sm" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showXIcon && (
|
||||||
|
<span
|
||||||
|
ref={iconRef}
|
||||||
|
className="relative z-[1] inline-flex scale-0 opacity-0"
|
||||||
|
>
|
||||||
|
<XIcon className="size-3 text-white drop-shadow-sm" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
!minimal && "text-body-sm font-medium",
|
||||||
|
"text-dark transition-colors duration-200 group-hover:text-primary dark:text-white dark:group-hover:text-primary/90",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { GlobeIcon, SearchIcon } from "@/assets/icons";
|
import { GlobeIcon, SearchIcon } from "@/assets/icons";
|
||||||
|
import { Checkbox } from "@/components/FormElements/checkbox";
|
||||||
import DatePicker from "@/components/FormElements/DatePicker/DatePicker";
|
import DatePicker from "@/components/FormElements/DatePicker/DatePicker";
|
||||||
import InputGroup from "@/components/FormElements/InputGroup";
|
import InputGroup from "@/components/FormElements/InputGroup";
|
||||||
import { Select } from "@/components/FormElements/select";
|
import { Select } from "@/components/FormElements/select";
|
||||||
@@ -67,6 +68,7 @@ function countActiveFilters(values: Record<string, unknown>): number {
|
|||||||
for (const key of RANGE_KEYS) {
|
for (const key of RANGE_KEYS) {
|
||||||
if (isRangeActive(values[key])) n++;
|
if (isRangeActive(values[key])) n++;
|
||||||
}
|
}
|
||||||
|
if (values.documents_scraped === true) n++;
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +217,21 @@ const TenderListFilters = ({
|
|||||||
</div>
|
</div>
|
||||||
</CollapsibleFilterSection>
|
</CollapsibleFilterSection>
|
||||||
|
|
||||||
|
<CollapsibleFilterSection
|
||||||
|
title="Document state"
|
||||||
|
subtitle="Restrict to tenders whose documents have been scraped."
|
||||||
|
>
|
||||||
|
<div className="max-w-fit rounded-xl border border-stroke/40 bg-white/60 p-4 dark:border-white/[0.06] dark:bg-dark-3/40">
|
||||||
|
<Checkbox
|
||||||
|
label="Documents scraped"
|
||||||
|
minimal
|
||||||
|
withBg
|
||||||
|
withIcon="check"
|
||||||
|
{...filterRegister("documents_scraped")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CollapsibleFilterSection>
|
||||||
|
|
||||||
<CollapsibleFilterSection
|
<CollapsibleFilterSection
|
||||||
title="Date ranges"
|
title="Date ranges"
|
||||||
subtitle="Unix-backed ranges — pick start/end; cleared ranges are omitted from the query."
|
subtitle="Unix-backed ranges — pick start/end; cleared ranges are omitted from the query."
|
||||||
|
|||||||
Reference in New Issue
Block a user