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_FIREBASE_API_KEY=AIzaSyCTjdsk2jE34IfnvSKP1JMTIf_Abd7tbt0
|
||||
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=opplens-270d1.firebaseapp.com
|
||||
|
||||
@@ -113,9 +113,9 @@ export default function FormElementsPage() {
|
||||
title="Checkbox and radio"
|
||||
className="space-y-5.5 !p-6.5"
|
||||
>
|
||||
{/* <Checkbox label="Checkbox Text" />
|
||||
<Checkbox label="Checkbox Text" withIcon="check" />
|
||||
<Checkbox label="Checkbox Text" withIcon="x" /> */}
|
||||
<Checkbox label="Checkbox Text" withIcon="check" withBg />
|
||||
<Checkbox label="With check icon" withIcon="check" withBg />
|
||||
<Checkbox label="With X icon" withIcon="x" withBg />
|
||||
<RadioInput label="Checkbox Text" />
|
||||
<RadioInput label="Checkbox Text" variant="circle" />
|
||||
</ShowcaseSection>
|
||||
|
||||
@@ -1,69 +1,238 @@
|
||||
"use client";
|
||||
|
||||
import { CheckIcon, XIcon } from "@/assets/icons";
|
||||
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 = {
|
||||
withIcon?: "check" | "x";
|
||||
withIcon?: "check" | "x" | "none";
|
||||
withBg?: boolean;
|
||||
label: string;
|
||||
name?: string;
|
||||
minimal?: boolean;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
radius?: "default" | "md";
|
||||
};
|
||||
className?: string;
|
||||
} & Omit<InputHTMLAttributes<HTMLInputElement>, "type">;
|
||||
|
||||
export function Checkbox({
|
||||
withIcon,
|
||||
function prefersReducedMotion() {
|
||||
return (
|
||||
typeof window !== "undefined" &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
);
|
||||
}
|
||||
|
||||
export const Checkbox = forwardRef<HTMLInputElement, PropsType>(function Checkbox(
|
||||
{
|
||||
withIcon = "check",
|
||||
label,
|
||||
name,
|
||||
withBg,
|
||||
withBg = true,
|
||||
minimal,
|
||||
onChange,
|
||||
radius,
|
||||
}: PropsType) {
|
||||
const id = useId();
|
||||
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 (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={cn(
|
||||
"flex cursor-pointer select-none items-center",
|
||||
!minimal && "text-body-sm font-medium",
|
||||
"group flex cursor-pointer select-none items-center",
|
||||
disabled && "cursor-not-allowed opacity-55",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="relative shrink-0">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="checkbox"
|
||||
onChange={onChange}
|
||||
name={name}
|
||||
id={id}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
checked={checked}
|
||||
defaultChecked={defaultChecked}
|
||||
onChange={handleChange}
|
||||
className="peer sr-only"
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
<div
|
||||
ref={boxRef}
|
||||
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",
|
||||
"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",
|
||||
)}
|
||||
>
|
||||
{!withIcon && (
|
||||
<span className="hidden size-2.5 rounded-sm bg-primary" />
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
|
||||
{withIcon === "check" && (
|
||||
<CheckIcon className="hidden text-primary" />
|
||||
{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"
|
||||
/>
|
||||
)}
|
||||
|
||||
{withIcon === "x" && <XIcon className="hidden text-primary" />}
|
||||
{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>{label}</span>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { GlobeIcon, SearchIcon } from "@/assets/icons";
|
||||
import { Checkbox } from "@/components/FormElements/checkbox";
|
||||
import DatePicker from "@/components/FormElements/DatePicker/DatePicker";
|
||||
import InputGroup from "@/components/FormElements/InputGroup";
|
||||
import { Select } from "@/components/FormElements/select";
|
||||
@@ -67,6 +68,7 @@ function countActiveFilters(values: Record<string, unknown>): number {
|
||||
for (const key of RANGE_KEYS) {
|
||||
if (isRangeActive(values[key])) n++;
|
||||
}
|
||||
if (values.documents_scraped === true) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -215,6 +217,21 @@ const TenderListFilters = ({
|
||||
</div>
|
||||
</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
|
||||
title="Date ranges"
|
||||
subtitle="Unix-backed ranges — pick start/end; cleared ranges are omitted from the query."
|
||||
|
||||
Reference in New Issue
Block a user