[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 { useEffect, useState } from "preact/hooks";
|
||||||
import { CheckboxInput, DefaultSeparator, FilterButton, LocationCard } from "../../components";
|
import { CheckboxInput, DefaultSeparator, FilterButton, LocationCard, SpinnerLoading } from "../../components";
|
||||||
import { useClick, useFloating, useInteractions, Side, AlignedPlacement, flip, shift, useDismiss } from "@floating-ui/react";
|
|
||||||
import { JSXInternal } from "node_modules/preact/src/jsx";
|
|
||||||
import { getListRecentLocationsRatingsService, getRegenciesService, } from "../../services";
|
import { getListRecentLocationsRatingsService, getRegenciesService, } from "../../services";
|
||||||
import { LocationInfo, Regency } from "../../domains";
|
import { LocationInfo, Regency } from "../../domains";
|
||||||
import { ChangeEvent, TargetedEvent } from "preact/compat";
|
|
||||||
import { LocationType } from "../../types/common";
|
import { LocationType } from "../../types/common";
|
||||||
import { enumKeys } from "../../utils";
|
import { enumKeys } from "../../utils";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import './style.css';
|
import { FloatFilter } from "../../components/Filter/FloatFilter";
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function Discovery() {
|
function Discovery() {
|
||||||
|
|
||||||
@ -129,20 +25,32 @@ function Discovery() {
|
|||||||
locationType: []
|
locationType: []
|
||||||
});
|
});
|
||||||
const [isFloatFilterOpen, setFloatFilterOpen] = useState(false)
|
const [isFloatFilterOpen, setFloatFilterOpen] = useState(false)
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getLocationType()
|
loadData()
|
||||||
getRecentLocations()
|
|
||||||
getRegion()
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
getLocationType(),
|
||||||
|
getRecentLocations(),
|
||||||
|
getRegion()
|
||||||
|
])
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getRegion() {
|
async function getRegion() {
|
||||||
try {
|
try {
|
||||||
const res = await getRegenciesService();
|
const res = await getRegenciesService();
|
||||||
setData((prevState) => ({ ...prevState, regencies: res.data, searchRegencies: res.data }))
|
setData((prevState) => ({ ...prevState, regencies: res.data, searchRegencies: res.data }))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err)
|
alert("Something went wrong when fetch regions / regencies")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,35 +156,50 @@ function Discovery() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="content main-content mt-3">
|
<div className="content main-content mt-3">
|
||||||
<section name="header">
|
<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 />
|
<DefaultSeparator />
|
||||||
</section>
|
</section>
|
||||||
<section name="content">
|
<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 */}
|
{/* FILTER */}
|
||||||
<section name="discovery filter">
|
<section name="discovery filter">
|
||||||
<div className="filter-container">
|
<div className="w-[195px] flex-col justify-start items-start gap-6 inline-flex mr-5 max-[920px]:hidden">
|
||||||
<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 className="self-stretch px-3 py-2 bg-[#36383B] rounded-lg justify-start items-center gap-2.5 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="flex-1 text-white text-base leading-6 tracking-wider break-words">Filter</div>
|
||||||
</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 className="self-stretch px-3 flex-col justify-start items-start gap-4 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 className="self-stretch text-white text-sm font-bold leading-[21px] tracking-wider break-words">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 flex-col justify-start items-start gap-3 flex">
|
||||||
{data.regencies.filter(x => x.isSelected).map(x => (
|
{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 className="self-stretch justify-start items-center gap-3 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>
|
<div className="flex-1 text-white text-sm font-medium leading-[21px] tracking-wider break-words">{x.regency_name}</div>
|
||||||
<button onClick={() => onDeleteByCityFilter(x)} style={{ color: "white"}}>x</button>
|
<button onClick={() => onDeleteByCityFilter(x)} className="text-white">x</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style="align-self: stretch; padding: 8px; border-radius: 6px; justify-content: center; align-items: center; gap: 4px; display: inline-flex">
|
<div className="self-stretch p-2 rounded-md justify-center items-center gap-1 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>
|
<button onClick={() => setFloatFilterOpen(!isFloatFilterOpen)} className="text-center text-green text-sm font-semibold leading-[21px] tracking-tight break-words">Lihat Selengkapnya</button>
|
||||||
<FloatFilter
|
<FloatFilter
|
||||||
isOpen={isFloatFilterOpen}
|
isOpen={isFloatFilterOpen}
|
||||||
placement="right"
|
placement="right"
|
||||||
|
title="Choose City / Regions"
|
||||||
listData={data.searchRegencies}
|
listData={data.searchRegencies}
|
||||||
outsidePress={() => setFloatFilterOpen(false)}
|
outsidePress={() => setFloatFilterOpen(false)}
|
||||||
onClearFilter={onClearFilter}
|
onClearFilter={onClearFilter}
|
||||||
@ -285,10 +208,10 @@ function Discovery() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
<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 className="self-stretch h-[190px] px-3 flex-col justify-start items-start gap-4 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 className="self-stretch text-white text-sm font-bold leading-[21px] tracking-wider break-words">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-[153px] flex-col justify-start items-start gap-3 flex">
|
||||||
{data.locationType.map((x, i) => (
|
{data.locationType.map((x, i) => (
|
||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
inputName="location_type"
|
inputName="location_type"
|
||||||
@ -299,7 +222,7 @@ function Discovery() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</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" />
|
<FilterButton onClick={onApplyFilter} label="Apply" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -307,7 +230,7 @@ function Discovery() {
|
|||||||
<div>
|
<div>
|
||||||
{data.locations.map(x => (
|
{data.locations.map(x => (
|
||||||
<LocationCard
|
<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}
|
onCardClick={onClickLocation}
|
||||||
data={x}
|
data={x}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user