[REFACTOR]: move float filter to its own component

This commit is contained in:
goro 2025-11-21 20:15:03 +07:00
parent 8a5281a0f2
commit 80fa81ae4b
2 changed files with 156 additions and 129 deletions

View File

@ -0,0 +1,104 @@
import { useClick, useFloating, useInteractions, Side, AlignedPlacement, flip, shift, useDismiss } from "@floating-ui/react";
import { TargetedEvent, useState, ChangeEvent } from "preact/compat";
import { JSXInternal } from "node_modules/preact/src/jsx";
import { Regency } from "src/domains";
interface floatFilterProps<T> {
isOpen: boolean,
placement: Side | AlignedPlacement,
outsidePress?: () => void,
onChecked: (e: TargetedEvent<HTMLInputElement, Event>, id: number) => void,
onClearFilter?: () => void,
onInputChange: (val: string) => void,
listData?: T[];
title: string;
}
export function FloatFilter(props: floatFilterProps<Regency & { isSelected?: boolean }>) {
const [floatState, setFloatState] = useState({
searchValue: "",
})
const { refs, floatingStyles, context } = useFloating({
placement: props.placement,
open: props.isOpen,
middleware: [
flip({ fallbackAxisSideDirection: "end" }),
shift()
]
});
const click = useClick(context);
const dismiss = useDismiss(context, {
outsidePress: () => {
props.outsidePress && props.outsidePress()
return false
}
});
const { getReferenceProps, getFloatingProps } = useInteractions([
click,
dismiss
]);
const style: JSXInternal.CSSProperties = {
...floatingStyles,
top: 210,
left: 'none',
marginLeft: 150,
width: 355,
zIndex: 1,
}
function onInputChange(e: ChangeEvent<HTMLInputElement>): void {
const input = e.target as HTMLInputElement;
setFloatState({ searchValue: input.value.toLowerCase() })
props.onInputChange(input.value)
}
if (props.isOpen) return (
<div
ref={refs.setFloating} {...getReferenceProps()}
style={style}
className="bg-secondary scrollbar-thin scrollbar-thumb-[#888] scrollbar-track-[#e0e0e0] hover:scrollbar-thumb-[#555] scrollbar-thumb-rounded"
{...getFloatingProps()}
>
<div className="p-2.5">
<p>{props.title}</p>
<input
type="text"
value={floatState.searchValue}
onChange={onInputChange}
placeholder={"Search ..."}
className="bg-quartenary text-sm mt-3 input-text"
/>
</div>
<div className="overflow-y-scroll h-[315px] p-2.5 scrollbar-thin scrollbar-thumb-[#888] scrollbar-track-[#e0e0e0] hover:scrollbar-thumb-[#555]">
{props.listData ?
<ul>
{props.listData.map(x => (
<div className="flex items-center mb-1.5">
<input
type={"checkbox"}
id={x.id.toString()}
checked={x.isSelected}
value={x.id}
name={x.regency_name}
className="input-checkbox mr-2.5 w-[18px] h-[18px]"
onChange={(e) => props.onChecked(e, x.id)}
/>
<label htmlFor={x.id.toString()}>{x.regency_name}</label>
</div>
))}
</ul>
: null}
</div>
<div className="flex justify-around pb-2.5 pt-2.5">
<button onClick={props.onClearFilter}>Clear</button>
<button onClick={props.outsidePress} >Close</button>
</div>
</div>
)
return null
}

View File

@ -1,115 +1,11 @@
import { useEffect, useState } from "preact/hooks";
import { CheckboxInput, DefaultSeparator, FilterButton, LocationCard } from "../../components";
import { useClick, useFloating, useInteractions, Side, AlignedPlacement, flip, shift, useDismiss } from "@floating-ui/react";
import { JSXInternal } from "node_modules/preact/src/jsx";
import { CheckboxInput, DefaultSeparator, FilterButton, LocationCard, SpinnerLoading } from "../../components";
import { getListRecentLocationsRatingsService, getRegenciesService, } from "../../services";
import { LocationInfo, Regency } from "../../domains";
import { ChangeEvent, TargetedEvent } from "preact/compat";
import { LocationType } from "../../types/common";
import { enumKeys } from "../../utils";
import { useNavigate } from "react-router-dom";
import './style.css';
interface floatFilterArgs<T> {
isOpen: boolean,
placement: Side | AlignedPlacement,
outsidePress?: () => void,
onChecked: (e: TargetedEvent<HTMLInputElement, Event>, id: number) => void,
onClearFilter?: () => void,
onInputChange: (val: string) => void,
listData?: T[];
}
function FloatFilter(props: floatFilterArgs<Regency & { isSelected?: boolean }>) {
const [floatState, setFloatState] = useState({
searchValue: "",
})
const { refs, floatingStyles, context } = useFloating({
placement: props.placement,
open: props.isOpen,
middleware: [
flip({ fallbackAxisSideDirection: "end" }),
shift()
]
});
const click = useClick(context);
const dismiss = useDismiss(context, {
outsidePress: () => {
props.outsidePress && props.outsidePress()
return false
}
});
const { getReferenceProps, getFloatingProps } = useInteractions([
click,
dismiss
]);
const style: JSXInternal.CSSProperties = {
...floatingStyles,
top: 210,
left: 'none',
marginLeft: 150,
width: 355,
zIndex: 1,
}
function onInputChange(e: ChangeEvent<HTMLInputElement>): void {
const input = e.target as HTMLInputElement;
setFloatState({ searchValue: input.value.toLowerCase() })
props.onInputChange(input.value)
}
if (props.isOpen) return (
<div
ref={refs.setFloating} {...getReferenceProps()}
style={style}
className={'bg-secondary float-filter-scroll'}
{...getFloatingProps()}
>
<div style={{ padding: 10 }}>
<input
type="text"
value={floatState.searchValue}
onChange={onInputChange}
placeholder={"Search ..."}
className={'bg-quartenary text-sm input-text mb-3'}
/>
</div>
<div style={{ overflowY: 'scroll', height: 315, padding: 10 }} className={'float-filter-scroll'}>
{props.listData ?
<ul>
{props.listData.map(x => (
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 5 }}>
<input
type={"checkbox"}
id={x.id.toString()}
checked={x.isSelected}
value={x.id}
name={x.regency_name}
className={'input-checkbox'}
style={{ marginRight: 10, width: 18, height: 18 }}
onChange={(e) => props.onChecked(e, x.id)}
/>
<label htmlFor={x.id.toString()}>{x.regency_name}</label>
</div>
))}
</ul>
: null}
</div>
<div style={{ display: 'flex', justifyContent: 'space-around', paddingBottom: 10, paddingTop: 10 }}>
<button onClick={props.onClearFilter}>Clear</button>
<button onClick={props.outsidePress} >Close</button>
</div>
</div>
)
return null
}
import { FloatFilter } from "../../components/Filter/FloatFilter";
function Discovery() {
@ -129,20 +25,32 @@ function Discovery() {
locationType: []
});
const [isFloatFilterOpen, setFloatFilterOpen] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const navigate = useNavigate()
useEffect(() => {
getLocationType()
getRecentLocations()
getRegion()
loadData()
}, [])
async function loadData() {
setIsLoading(true)
try {
await Promise.all([
getLocationType(),
getRecentLocations(),
getRegion()
])
} finally {
setIsLoading(false)
}
}
async function getRegion() {
try {
const res = await getRegenciesService();
setData((prevState) => ({ ...prevState, regencies: res.data, searchRegencies: res.data }))
} catch (err) {
alert(err)
alert("Something went wrong when fetch regions / regencies")
}
}
@ -248,35 +156,50 @@ function Discovery() {
})
}
if (isLoading) {
return (
<div className="content main-content mt-3">
<section name="header">
<h1 className={'text-3xl mb-5 font-bold'}>Discovery</h1>
<h1 className="text-3xl mb-5 font-bold">Discovery</h1>
<DefaultSeparator />
</section>
<div className="flex justify-center items-center min-h-[400px]">
<SpinnerLoading style={{ width: '50px', height: '50px' }} />
</div>
</div>
)
}
return (
<div className="content main-content mt-3">
<section name="header">
<h1 className="text-3xl mb-5 font-bold">Discovery</h1>
<DefaultSeparator />
</section>
<section name="content">
<div style="padding-bottom: 24px; justify-content: flex-start; align-items: flex-start; display: inline-flex">
<div className="pb-6 justify-start items-start inline-flex">
{/* FILTER */}
<section name="discovery filter">
<div className="filter-container">
<div style="align-self: stretch; padding-left: 12px; padding-right: 12px; padding-top: 8px; padding-bottom: 8px; background: #36383B; border-radius: 8px; justify-content: flex-start; align-items: center; gap: 10px; display: inline-flex">
<div style="flex: 1 1 0; color: white; font-size: 16px; line-height: 24px; letter-spacing: 0.64px; word-wrap: break-word">Filter</div>
<div className="w-[195px] flex-col justify-start items-start gap-6 inline-flex mr-5 max-[920px]:hidden">
<div className="self-stretch px-3 py-2 bg-[#36383B] rounded-lg justify-start items-center gap-2.5 inline-flex">
<div className="flex-1 text-white text-base leading-6 tracking-wider break-words">Filter</div>
</div>
<div style="align-self: stretch; padding-left: 12px; padding-right: 12px; flex-direction: column; justify-content: flex-start; align-items: flex-start; gap: 16px; display: flex">
<div style="align-self: stretch; color: white; font-size: 14px; font-weight: 700; line-height: 21px; letter-spacing: 0.56px; word-wrap: break-word">Kota / Kabupaten </div>
<div style="align-self: stretch; flex-direction: column; justify-content: flex-start; align-items: flex-start; gap: 12px; display: flex">
<div className="self-stretch px-3 flex-col justify-start items-start gap-4 flex">
<div className="self-stretch text-white text-sm font-bold leading-[21px] tracking-wider break-words">Kota / Kabupaten </div>
<div className="self-stretch flex-col justify-start items-start gap-3 flex">
{data.regencies.filter(x => x.isSelected).map(x => (
<div style="align-self: stretch; justify-content: flex-start; align-items: center; gap: 12px; display: inline-flex">
<div style="flex: 1 1 0; color: white; font-size: 14px; font-family: Poppins; font-weight: 500; line-height: 21px; letter-spacing: 0.56px; word-wrap: break-word">{x.regency_name}</div>
<button onClick={() => onDeleteByCityFilter(x)} style={{ color: "white"}}>x</button>
<div className="self-stretch justify-start items-center gap-3 inline-flex">
<div className="flex-1 text-white text-sm font-medium leading-[21px] tracking-wider break-words">{x.regency_name}</div>
<button onClick={() => onDeleteByCityFilter(x)} className="text-white">x</button>
</div>
))}
</div>
<div style="align-self: stretch; padding: 8px; border-radius: 6px; justify-content: center; align-items: center; gap: 4px; display: inline-flex">
<button onClick={() => setFloatFilterOpen(!isFloatFilterOpen)} style="text-align: center; color: #85CE73; font-size: 14px; font-weight: 600; line-height: 21px; letter-spacing: 0.10px; word-wrap: break-word">Lihat Selengkapnya</button>
<div className="self-stretch p-2 rounded-md justify-center items-center gap-1 inline-flex">
<button onClick={() => setFloatFilterOpen(!isFloatFilterOpen)} className="text-center text-green text-sm font-semibold leading-[21px] tracking-tight break-words">Lihat Selengkapnya</button>
<FloatFilter
isOpen={isFloatFilterOpen}
placement="right"
title="Choose City / Regions"
listData={data.searchRegencies}
outsidePress={() => setFloatFilterOpen(false)}
onClearFilter={onClearFilter}
@ -285,10 +208,10 @@ function Discovery() {
/>
</div>
</div>
<div style="align-self: stretch; height: 2px; background: #36383B; border-radius: 2px"></div>
<div style="align-self: stretch; height: 190px; padding-left: 12px; padding-right: 12px; flex-direction: column; justify-content: flex-start; align-items: flex-start; gap: 16px; display: flex">
<div style="align-self: stretch; color: white; font-size: 14px; font-family: Poppins; font-weight: 700; line-height: 21px; letter-spacing: 0.56px; word-wrap: break-word">Tipe</div>
<div style="align-self: stretch; height: 153px; flex-direction: column; justify-content: flex-start; align-items: flex-start; gap: 12px; display: flex">
<div className="self-stretch h-0.5 bg-[#36383B] rounded-sm"></div>
<div className="self-stretch h-[190px] px-3 flex-col justify-start items-start gap-4 flex">
<div className="self-stretch text-white text-sm font-bold leading-[21px] tracking-wider break-words">Tipe</div>
<div className="self-stretch h-[153px] flex-col justify-start items-start gap-3 flex">
{data.locationType.map((x, i) => (
<CheckboxInput
inputName="location_type"
@ -299,7 +222,7 @@ function Discovery() {
))}
</div>
</div>
<div style="align-self: stretch; height: 2px; background: #36383B; border-radius: 2px"></div>
<div className="self-stretch h-0.5 bg-[#36383B] rounded-sm"></div>
<FilterButton onClick={onApplyFilter} label="Apply" />
</div>
</section>
@ -307,7 +230,7 @@ function Discovery() {
<div>
{data.locations.map(x => (
<LocationCard
containerClass='card-list-container'
containerClass="p-[10px_1%_15px] inline-block m-[0_0_15px] align-top w-[20%] max-[1300px]:w-[24.3%] max-[1135px]:w-[25%] max-[1100px]:w-[33%] max-[920px]:w-[33%] max-[625px]:w-[50%]"
onCardClick={onClickLocation}
data={x}
/>