Skip to content

Commit

Permalink
Add Area search filter
Browse files Browse the repository at this point in the history
refs #49
  • Loading branch information
franthormel committed Aug 18, 2024
1 parent fe78717 commit 5caf46e
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 39 deletions.
10 changes: 10 additions & 0 deletions app/listings/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,13 @@ export const BEDS_BATHS_FILTER = ["Any", "1", "2", "3", "4", "5+"];
* * Default value is `Any`
*/
export const BEDS_BATHS_DEFAULT = 0;

export const AREA_MIN_FILTER = 0;
/**
* Placeholder value for maximum area input field.
*/
export const AREA_MAX_INPUT = 1_000;
/**
* Maximum input value for the maximum area input field.
*/
export const AREA_MAX_FILTER = 1_000_000;
5 changes: 4 additions & 1 deletion app/listings/filter-beds-baths.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ export default function ListingsFiltersBedsBaths() {
}} />
<ButtonSmallFilled text="Search"
onClick={() => {
// Close dropdown ...
setDisplayDropdown(false)

// ... change filter values
context.searchFilters.beds.change(beds)
context.searchFilters.baths.change(baths)
setDisplayDropdown(false)
}} />
</div>
</div>
Expand Down
21 changes: 4 additions & 17 deletions app/listings/filter-price.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,14 @@ export default function ListingsFilterPrice() {
const result = minPriceValidator.safeParse(value)

if (result.success) {
// Only clear existing error messages
if (minPriceError !== undefined) {
setMinPriceError(undefined)
}

setMinPriceError(undefined)
// Only change value if validation is successful
setMaxPriceValidator(customizePriceValidator(value, PRICE_MAX_FILTER))
setMinPrice(value)
} else {
// Only change if it is a different error message
const error = result.error.errors[0].message
if (minPriceError !== error) {
setMinPriceError(error)
}
setMinPriceError(error)
}
}} />
<FormInput label="Maximum Price"
Expand All @@ -81,20 +75,13 @@ export default function ListingsFilterPrice() {
const result = maxPriceValidator.safeParse(value)

if (result.success) {
// Only clear existing error messages
if (maxPriceError !== undefined) {
setMaxPriceError(undefined)
}

setMaxPriceError(undefined)
// Only change value if validation is successful
setMinPriceValidator(customizePriceValidator(PRICE_MIN_FILTER, value))
setMaxPrice(value)
} else {
// Only change if it is a different error message
const error = result.error.errors[0].message
if (maxPriceError !== error) {
setMaxPriceError(error)
}
setMaxPriceError(error)
}
}} />
<div className="flex justify-evenly">
Expand Down
98 changes: 85 additions & 13 deletions app/listings/filters-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,28 @@ import ButtonSmallFilled from "@/components/buttons/small/filled"
import ButtonSmallText from "@/components/buttons/small/text"
import Dropdown from "@/components/dropdown"
import FormInput from "@/components/form-input"
import { NumberUtils } from "@/lib/commons/number_utils"
import { formatAppend } from "@/lib/formatter/number"
import { useState } from "react"
import { customizeAreaValidator } from "@/lib/validation/listing/validators"
import { useContext, useState } from "react"
import { ZodNumber } from "zod"
import { AREA_MAX_FILTER, AREA_MAX_INPUT, AREA_MIN_FILTER } from "./constants"
import { ListingsContext } from "./provider"

export default function ListingsFilterArea() {
const DEFAULT_MAX = 20
const maxAreaPlaceholder = formatAppend(AREA_MAX_INPUT, "sqm.")

const minAreaPlaceholder = "None"
const maxAreaPlaceholder = formatAppend(DEFAULT_MAX, "sqm.")
const [minArea, setMinArea] = useState<number>(AREA_MIN_FILTER)
const [maxArea, setMaxArea] = useState<number>(AREA_MAX_FILTER)

const [minAreaError, setMinAreaError] = useState<string | undefined>(undefined)
const [maxAreaError, setMaxAreaError] = useState<string | undefined>(undefined)

const defaultValidator = customizeAreaValidator(AREA_MIN_FILTER, AREA_MAX_FILTER)
const [minAreaValidator, setMinAreaValidator] = useState<ZodNumber>(defaultValidator)
const [maxAreaValidator, setMaxAreaValidator] = useState<ZodNumber>(defaultValidator)

const context = useContext(ListingsContext)
const [displayDropdown, setDisplayDropdown] = useState<boolean>(false)

return (
Expand All @@ -27,17 +40,76 @@ export default function ListingsFilterArea() {
<div className="flex w-24 flex-col gap-5 xl:w-56">
<FormInput label="Minimum Area"
name="area-min"
placeholder={minAreaPlaceholder} />
type="number"
placeholder="None"
value={minArea}
min={AREA_MIN_FILTER}
max={maxArea}
errorMessage={minAreaError}
onChange={(e) => {
const value = NumberUtils.toNumber(e.target.value, -1)
const result = minAreaValidator.safeParse(value)

if (result.success) {
setMinAreaError(undefined)
// Only change value if validation is successful
setMaxAreaValidator(customizeAreaValidator(value, AREA_MAX_FILTER))
setMinArea(value)
} else {
const error = result.error.errors[0].message
setMinAreaError(error)
}
}} />
<FormInput label="Maximum Area"
name="area-min"
placeholder={maxAreaPlaceholder} />
<div className="flex flex-col gap-3 xl:hidden">
<ButtonSmallText text="Reset" />
<ButtonSmallFilled text="Search" />
</div>
<div className="hidden justify-evenly xl:flex">
<ButtonSmallText text="Reset" />
<ButtonSmallFilled text="Search" />
type="number"
placeholder={maxAreaPlaceholder}
value={maxArea}
min={minArea}
max={AREA_MAX_FILTER}
errorMessage={maxAreaError}
onChange={(e) => {
const value = NumberUtils.toNumber(e.target.value, -1)
const result = maxAreaValidator.safeParse(value)

if (result.success) {
setMaxAreaError(undefined)
// Only change value if validation is successful
setMinAreaValidator(customizeAreaValidator(AREA_MIN_FILTER, value))
setMaxArea(value)
} else {
const error = result.error.errors[0].message
setMaxAreaError(error)
}
}} />
<div className="flex flex-col gap-3 xl:flex-row xl:justify-evenly xl:gap-0">
<ButtonSmallText text="Reset"
onClick={(e) => {
// Reset input values ...
setMinArea(AREA_MIN_FILTER)
setMaxArea(AREA_MAX_FILTER)

// ... error messages ...
setMinAreaError(undefined)
setMaxAreaError(undefined)

// ... and validators
setMinAreaValidator(defaultValidator)
setMaxAreaValidator(defaultValidator)
}} />
<ButtonSmallFilled text="Search"
onClick={(e) => {
// Close dropdown ...
setDisplayDropdown(false)

// ... error messages ...
setMinAreaError(undefined)
setMaxAreaError(undefined)

// ... change filter values
context.searchFilters.area.min.change(minArea)
context.searchFilters.area.max.change(maxArea)
}} />
</div>
</div>
</Dropdown>
Expand Down
42 changes: 36 additions & 6 deletions app/listings/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
"use client"

import { createContext, useState } from "react"
import { BEDS_BATHS_DEFAULT, PRICE_MAX_FILTER, PRICE_MIN_FILTER, STARTING_PAGE } from "./constants"
import { AREA_MAX_INPUT, AREA_MIN_FILTER, BEDS_BATHS_DEFAULT, PRICE_MAX_FILTER, PRICE_MIN_FILTER, STARTING_PAGE } from "./constants"

type ContextTypeNumber = {
value: number,
change: (value: number) => void
}

type ContenTypeNumberRange = {
min: ContextTypeNumber,
max: ContextTypeNumber
}

interface ListingsContextInterface {
searchFilters: {
price: {
min: ContextTypeNumber,
max: ContextTypeNumber
},
price: ContenTypeNumberRange,
beds: ContextTypeNumber,
baths: ContextTypeNumber
baths: ContextTypeNumber,
area: ContenTypeNumberRange
},
pagination: {
changeToPreviousPage: () => void,
Expand Down Expand Up @@ -44,6 +47,16 @@ export const ListingsContext = createContext<ListingsContextInterface>({
value: BEDS_BATHS_DEFAULT,
change: (value) => { }
},
area: {
min: {
value: AREA_MIN_FILTER,
change: (value) => { }
},
max: {
value: AREA_MAX_INPUT,
change: (value) => { }
}
},
},
pagination: {
changeToPreviousPage: () => { },
Expand All @@ -62,6 +75,9 @@ export default function ListingsProvider({ children }: { children: React.ReactNo
const [bedsFilter, setBedsFilter] = useState<number>(BEDS_BATHS_DEFAULT)
const [bathsFilter, setBathsFilter] = useState<number>(BEDS_BATHS_DEFAULT)

const [minAreaFilter, setMinAreaFilter] = useState<number>(AREA_MIN_FILTER)
const [maxAreaFilter, setMaxAreaFilter] = useState<number>(AREA_MAX_INPUT)

const [currentPage, setCurrentPage] = useState<number>(STARTING_PAGE);

const stateValue: ListingsContextInterface = {
Expand Down Expand Up @@ -91,6 +107,20 @@ export default function ListingsProvider({ children }: { children: React.ReactNo
change: (value) => {
setBathsFilter(value)
}
},
area: {
min: {
value: minAreaFilter,
change: (value) => {
setMinAreaFilter(value)
}
},
max: {
value: maxAreaFilter,
change: (value) => {
setMaxAreaFilter(value)
}
}
}
},
pagination: {
Expand Down
1 change: 1 addition & 0 deletions app/listings/search-filters-modal-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ListingsFilterPrice from "./filter-price";
import ListingsFilterArea from "./filters-area";

export default function ListingsSearchFiltersModalContent() {
// TODO: Fix display of search filters
return (
<div className="flex flex-col gap-8 p-8">
<header className="text-2xl font-bold">
Expand Down
21 changes: 19 additions & 2 deletions lib/validation/listing/validators.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NUMBER_FORMATTER } from "@/lib/formatter/number";
import { z } from "zod";
import { CURRENCY_FORMATTER } from "../../formatter/currency";

Expand Down Expand Up @@ -121,13 +122,29 @@ export const AREA_VALIDATOR = z
})
.min(AREA_MIN, {
// FUTURE: Localize `sqm`
message: `Area must be at least ${AREA_MIN} sqm.`,
message: `Area must be at least ${NUMBER_FORMATTER.format(AREA_MIN)} sqm.`,
})
.max(AREA_MAX, {
// FUTURE: Localize `sqm`
message: `Area must not exceed ${AREA_MAX} sqm.`,
message: `Area must not exceed ${NUMBER_FORMATTER.format(AREA_MAX)} sqm.`,
});

export function customizeAreaValidator(min: number, max: number): z.ZodNumber {
return z
.number({
required_error: "Area is required",
invalid_type_error: "Area must be a number",
})
.min(min, {
// FUTURE: Localize `sqm`
message: `Area must be at least ${NUMBER_FORMATTER.format(min)} sqm.`,
})
.max(max, {
// FUTURE: Localize `sqm`
message: `Area must not exceed ${NUMBER_FORMATTER.format(max)} sqm.`,
});
}

// Available Date
// NOTE: Append min-max date values using this validator if needed
export const AVAILABLE_DATE_VALIDATOR = z.coerce.date({
Expand Down

0 comments on commit 5caf46e

Please sign in to comment.