Files
beer-inventory/app/routes/inventory/route.tsx
T
kennyboy55 b8a20204c7
Build dev docker image / build (push) Successful in 36s
Build dev docker image / release (push) Successful in 1s
AI Changes: updated look, recently added, search bar, easy admin checkout
2026-07-31 21:12:57 +02:00

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>
);
}