168 lines
6.0 KiB
TypeScript
168 lines
6.0 KiB
TypeScript
import { LoaderFunctionArgs, json, redirect } from "@remix-run/node";
|
|
import { useLoaderData, useNavigate, useSearchParams } from "@remix-run/react";
|
|
import { enhance } from "@zenstackhq/runtime";
|
|
import { useEffect, useState } from "react";
|
|
import { Button, Col, Container, Form, Offcanvas, Row } from "react-bootstrap";
|
|
import DrinkCard from "~/components/cards/drink.card";
|
|
import DrinkFilter from "~/components/filters/drink.filter";
|
|
import { findDrinksFromSearch } from "~/models/drinks.filter.server";
|
|
import { findManufacturersFromSearch } from "~/models/drinks.server";
|
|
import { sortDrinksFromSearch } from "~/models/drinks.sort.server";
|
|
|
|
import LinkContainer from "~/components/LinkContainer";
|
|
import { db } from "~/utils/db.server";
|
|
import { randomRange } from "~/utils/random";
|
|
|
|
export const loader = async ({request} : LoaderFunctionArgs) => {
|
|
const dbe = enhance(db);
|
|
|
|
const url = new URL(request.url);
|
|
|
|
const beerStyles = await dbe.beerStyle.findMany({orderBy: {name: "asc"}});
|
|
const wineStyles = await dbe.wineStyle.findMany({orderBy: {name: "asc"}});
|
|
const manufacturers = await findManufacturersFromSearch(url.searchParams);
|
|
|
|
const drinkResultsUnsorted = await findDrinksFromSearch(url.searchParams);
|
|
const drinkResults = sortDrinksFromSearch(url.searchParams, drinkResultsUnsorted);
|
|
|
|
return json({beerStyles, wineStyles, manufacturers, drinkResults});
|
|
};
|
|
|
|
export default function BeersRoute() {
|
|
const loadData = useLoaderData<typeof loader>();
|
|
|
|
const navigate = useNavigate();
|
|
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [searchQuery, setSearchQuery] = useState(searchParams.get("q") || "");
|
|
|
|
const [show, setShow] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setSearchQuery(searchParams.get("q") || "");
|
|
}, [searchParams]);
|
|
|
|
useEffect(() => {
|
|
const timer = window.setTimeout(() => {
|
|
const nextParams = new URLSearchParams(searchParams);
|
|
const trimmedQuery = searchQuery.trim();
|
|
|
|
if (trimmedQuery) {
|
|
nextParams.set("q", trimmedQuery);
|
|
} else {
|
|
nextParams.delete("q");
|
|
}
|
|
|
|
const nextSearch = nextParams.toString();
|
|
const currentSearch = searchParams.toString();
|
|
|
|
if (nextSearch !== currentSearch) {
|
|
setSearchParams(nextParams, { replace: true });
|
|
}
|
|
}, 250);
|
|
|
|
return () => window.clearTimeout(timer);
|
|
}, [searchQuery, searchParams, setSearchParams]);
|
|
|
|
const handleClose = () => setShow(false);
|
|
const handleShow = () => setShow(true);
|
|
|
|
const activeFilterKeys = new Set<string>();
|
|
|
|
if (searchQuery.trim()) {
|
|
activeFilterKeys.add("q");
|
|
}
|
|
|
|
searchParams.forEach((value, key) => {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
|
|
if (key === "sort" && value === "name") {
|
|
return;
|
|
}
|
|
|
|
if ((key === "min-abv" && value === "0") || (key === "max-abv" && value === "50") || (key === "min-ibu" && value === "0") || (key === "max-ibu" && value === "150")) {
|
|
return;
|
|
}
|
|
|
|
activeFilterKeys.add(key);
|
|
});
|
|
|
|
const activeFilterCount = activeFilterKeys.size;
|
|
const hasActiveFilters = activeFilterCount > 0;
|
|
const numResults = loadData.drinkResults.length;
|
|
|
|
let randomDrink = function(){
|
|
let index = randomRange(0, numResults - 1);
|
|
let drink_id = loadData.drinkResults[index].id;
|
|
navigate("/inventory/drink/" + drink_id);
|
|
}
|
|
|
|
return (
|
|
<Container>
|
|
<Row key="filter-sort-bar" className="pt-1 sticky-top bg-white mb-2" style={{boxShadow: "0px 4px 4px -5px rgba(0,0,0,.5)"}}>
|
|
<Col key="filtering" className="mb-3">
|
|
<div className="d-flex flex-nowrap align-items-center gap-2" style={{ overflowX: 'auto' }}>
|
|
<Button
|
|
variant={hasActiveFilters ? "warning" : "primary"}
|
|
onClick={handleShow}
|
|
className="d-flex align-items-center gap-2 flex-shrink-0"
|
|
title={hasActiveFilters ? `${activeFilterCount} active filters` : "No active filters"}
|
|
>
|
|
<span>Filters</span>
|
|
{hasActiveFilters ? <span className="badge text-bg-light">{activeFilterCount}</span> : null}
|
|
</Button>
|
|
<Form.Control
|
|
type="search"
|
|
placeholder="Search drinks"
|
|
value={searchQuery}
|
|
onChange={(event) => setSearchQuery(event.target.value)}
|
|
className="flex-grow-1"
|
|
style={{ minWidth: 0, maxWidth: "420px" }}
|
|
/>
|
|
<Button variant="warning" onClick={() => navigate("/inventory")} className="flex-shrink-0">Clear</Button>
|
|
<span className="ms-auto text-nowrap">{numResults} Results</span>
|
|
</div>
|
|
|
|
<Offcanvas show={show} onHide={handleClose}>
|
|
<Offcanvas.Header closeButton>
|
|
<Offcanvas.Title>Sorting and filtering <br/> <span className="fs-6">{numResults} Results</span></Offcanvas.Title>
|
|
</Offcanvas.Header>
|
|
<Offcanvas.Body>
|
|
<DrinkFilter
|
|
beerStyles={loadData.beerStyles}
|
|
wineStyles={loadData.wineStyles}
|
|
manufacturers={loadData.manufacturers}
|
|
searchParams={searchParams} >
|
|
</DrinkFilter>
|
|
</Offcanvas.Body>
|
|
</Offcanvas>
|
|
</Col>
|
|
</Row>
|
|
<Row key="filter-sort-results">
|
|
<Col key="results">
|
|
<Row xs={2} md={3} lg={4} className="g-4">
|
|
{loadData.drinkResults.map((drink) => (
|
|
<Col >
|
|
<DrinkCard drink={drink}></DrinkCard>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
</Col>
|
|
</Row>
|
|
<Row key="filter-sort-autopick">
|
|
<Col key="pick">
|
|
<Row className="my-5">
|
|
<Col className="text-center">
|
|
<Button size="lg" onClick={randomDrink} >Pick one for me!</Button>
|
|
</Col>
|
|
</Row>
|
|
</Col>
|
|
</Row>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
|