[REFACTOR]: move float filter to its own component
This commit is contained in:
parent
8a5281a0f2
commit
80fa81ae4b
104
src/components/Filter/FloatFilter/index.tsx
Normal file
104
src/components/Filter/FloatFilter/index.tsx
Normal 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
|
||||
}
|
||||
@ -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>
|
||||
<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>
|
||||
<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}
|
||||
/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user