Add filtering, authentication and much more

This commit is contained in:
2024-05-11 11:13:28 +02:00
parent 864086cb67
commit c5e8f6e2e5
28 changed files with 2078 additions and 303 deletions
-23
View File
@@ -1,23 +0,0 @@
import { Link } from '@remix-run/react';
import { Beer } from '@zenstackhq/runtime/models';
import { Card } from 'react-bootstrap';
interface BeerArgments {
beer: Beer
}
function BeerCard(options:BeerArgments) {
return (
<Card style={{ width: '18rem' }}>
<Card.Body>
<Card.Title>{options.beer.name}</Card.Title>
<Card.Text>
{options.beer.description}
</Card.Text>
<Link to={options.beer.id.toString()}>Permalink</Link>
</Card.Body>
</Card>
);
}
export default BeerCard;
+63
View File
@@ -0,0 +1,63 @@
import { Link } from '@remix-run/react';
import { Card, ListGroup } from 'react-bootstrap';
import { DrinkComposite, isDrinkWithContainers, isDrinkWithManufacturer, isDrinkWithStyle } from '~/models/types';
interface Arguments {
drink: DrinkComposite
}
function DrinkCard(arg:Arguments) {
var trimmedDescription = arg.drink.description.length > 120 ? arg.drink.description.substring(0, 120) + "..." : arg.drink.description;
var link = "/drink/";
switch (arg.drink.type) {
case 'Beer':
link = "/beer/";
break;
case 'Wine':
link = "/wine/";
break;
case 'Soda':
link = "/soda/";
break;
}
var borderColor = "#bbb";
if(isDrinkWithStyle(arg.drink)){
borderColor = "#" + arg.drink.style.color;
}
var manufacturer = "";
if(isDrinkWithManufacturer(arg.drink)){
manufacturer = arg.drink.manufacturer.name + " (" + arg.drink.manufacturer.country_id + ")";
}
var inventory = "";
if(isDrinkWithContainers(arg.drink)){
inventory = "Inventory: " + arg.drink.containers.length;
}
return (
<Card key={arg.drink.slug} style={{ borderColor: `${borderColor}`, borderWidth: '2px' }} className='mb-2'>
<Card.Header className="text-muted">{manufacturer}</Card.Header>
<Card.Img variant="top" src={arg.drink.image ? arg.drink.image : ""} />
<Card.Body>
<Card.Title><Link to={link + arg.drink.slug.toString()} className='stretched-link text-reset text-decoration-none'>{arg.drink.name}</Link></Card.Title>
<Card.Text>
{trimmedDescription}
</Card.Text>
</Card.Body>
<ListGroup variant="flush">
<ListGroup.Item>ABV: {arg.drink.abv}%</ListGroup.Item>
{ isDrinkWithStyle(arg.drink) ? (
<ListGroup.Item>Style: {arg.drink.style.name}</ListGroup.Item>
) : ""}
<ListGroup.Item>Allergy: {arg.drink.gluten ? "G" : ""} {arg.drink.lactose ? "L" : ""} {arg.drink.organic ? "B" : ""}</ListGroup.Item>
</ListGroup>
<Card.Footer className="text-muted">{inventory}</Card.Footer>
</Card>
);
}
export default DrinkCard;
+80
View File
@@ -0,0 +1,80 @@
import { Link } from '@remix-run/react';
import { Card, Col, Container, Image, ListGroup, Row } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
import { DrinkComposite, isDrinkWithContainers, isDrinkWithManufacturer, isDrinkWithStyle } from '~/models/types';
interface Arguments {
drink: DrinkComposite
}
function DrinkPage(arg:Arguments) {
var trimmedDescription = arg.drink.description.length > 120 ? arg.drink.description.substring(0, 120) + "..." : arg.drink.description;
var link = "/drink/";
switch (arg.drink.type) {
case 'Beer':
link = "/beer/";
break;
case 'Wine':
link = "/wine/";
break;
case 'Soda':
link = "/soda/";
break;
}
var borderColor = "#bbb";
if(isDrinkWithStyle(arg.drink)){
borderColor = "#" + arg.drink.style.color;
}
var manufacturer = "";
if(isDrinkWithManufacturer(arg.drink)){
manufacturer = arg.drink.manufacturer.name + " (" + arg.drink.manufacturer.country_id + ")";
}
var inventory = "";
if(isDrinkWithContainers(arg.drink)){
inventory = "Inventory: " + arg.drink.containers.length;
}
return (
<Container>
<div className="p-5 mb-4 bg-body-tertiary rounded-3" style={{ borderColor: `${borderColor}`, borderWidth: '2px' }} >
<Container fluid>
<Row>
<Col md={11}>
<h1 className="display-5 fw-bold">{arg.drink.name}</h1>
<p className="col-md-8 fs-4">{arg.drink.description}</p>
</Col>
<Col md={1}>
<Image src={arg.drink.image ?? undefined} fluid rounded/>
</Col>
</Row>
</Container>
</div>
<Row className="align-items-md-stretch">
<Col md={6}>
<div className="h-100 p-5 text-bg-dark rounded-3">
<h2>Inventory</h2>
{arg.drink.containers.map((container) => (
<p>{container.section.name} | {container.type} | {container.inventory}</p>
))}
</div>
</Col>
<Col md={6}>
<div className="h-100 p-5 bg-body-tertiary border rounded-3">
<h2>{arg.drink.manufacturer.name}</h2>
<p>{arg.drink.manufacturer.description}</p>
</div>
</Col>
</Row>
</Container>
);
}
export default DrinkPage;
+168
View File
@@ -0,0 +1,168 @@
import { useSubmit } from '@remix-run/react';
import { BeerStyle, Manufacturer, WineStyle } from '@zenstackhq/runtime/models';
import { useEffect, useRef, useState } from 'react';
import { Button, Form, Col, Row } from 'react-bootstrap';
import ReactSlider from 'react-slider'
interface Arguments {
beerStyles: BeerStyle[];
wineStyles: WineStyle[];
manufacturers: Manufacturer[];
searchParams: URLSearchParams
}
function DrinkFilter(arg : Arguments) {
const submit = useSubmit();
const [abv, setAbv] = useState({min: Number(arg.searchParams.get("min-abv") || 0), max: Number(arg.searchParams.get("max-abv") || 50)})
const showBeerFilter = arg.searchParams.has("drink", "Beer");
const showWineFilter = arg.searchParams.has("drink", "Wine");
const showSodaFilter = arg.searchParams.has("drink", "Soda");
const showCocktailFilter = arg.searchParams.has("drink", "Cocktail");
const form = useRef(null);
const submitAfterRender = function(){
setTimeout(() => submit(form.current), 10);
}
const slider = () =>
(<div>
<ReactSlider
className="form-range"
thumbClassName="bg-primary text-light rounded dummy"
trackClassName="bg-secondary h-100 rounded dummy"
min={0}
max={50}
defaultValue={[abv.min, abv.max]}
renderThumb={(props, state) => <div {...props}>{state.valueNow}%</div>}
pearling
minDistance={2}
onAfterChange={(number, index) => {setAbv({min: number[0], max: number[1]}); submitAfterRender()}}
/>
<input type="hidden" name="min-abv" value={abv.min} />
<input type="hidden" name="max-abv" value={abv.max} />
</div>);
return (
<Form noValidate method="GET" ref={form} onChange={(event) => {submit(event.currentTarget); }}>
<Row>
<Col xs={6} md={12}>
<Form.Group controlId="drink-type">
<Form.Label>Drink type</Form.Label>
{ ["Beer", "Wine", "Soda", "Cocktail"].map(key => (
<Form.Check
id={"drink-" + key}
type="checkbox"
label={key}
name="drink"
value={key}
defaultChecked={arg.searchParams.has("drink", key) ? true : false}
/>
))}
</Form.Group>
<Form.Group controlId="drink-modifiers">
<Form.Label>Drink</Form.Label>
{ ["Gluten-free", "Lactose-free", "Organic"].map(key => (
<Form.Check
id={"drinkmod-" + key}
type="checkbox"
label={key}
name={key.toLowerCase()}
value={1}
defaultChecked={arg.searchParams.has(key.toLowerCase()) ? true : false}
/>
))}
<Form.Label>ABV</Form.Label>
{slider()}
</Form.Group>
<Form.Group controlId="manufacturer">
<Form.Label>Manufacturer</Form.Label>
{ arg.manufacturers.map(manufacturer => (
<Form.Check
id={"manufacturer-" + manufacturer.name}
type="checkbox"
label={manufacturer.name}
name="manufacturer"
value={manufacturer.id}
defaultChecked={arg.searchParams.has("manufacturer", manufacturer.id.toString()) ? true : false}
/>
))}
</Form.Group>
</Col>
<Col>
{ showBeerFilter ? (
<Form.Group controlId="beer-style">
<Form.Label>Beer style</Form.Label>
{ arg.beerStyles.map(style => (
<Form.Check
id={"beerstyle-" + style.name}
type="checkbox"
label={style.name}
name="beerstyle"
value={style.id}
defaultChecked={arg.searchParams.has("beerstyle", style.id.toString()) ? true : false}
/>
))}
</Form.Group>
) : ""}
{ showWineFilter ? (
<Form.Group controlId="wine-style">
<Form.Label>Wine style</Form.Label>
{ arg.wineStyles.map(style => (
<Form.Check
id={"winestyle-" + style.name}
type="checkbox"
label={style.name}
name="winestyle"
value={style.id}
defaultChecked={arg.searchParams.has("winestyle", style.id.toString()) ? true : false}
/>
))}
</Form.Group>
) : ""}
{ showSodaFilter ? (
<Form.Group controlId="soda-carbonated">
<Form.Label>Soda</Form.Label>
<Form.Check
type="checkbox"
label="Carbonated"
name="carbonated"
value={1}
defaultChecked={arg.searchParams.has("carbonated") ? true : false}
/>
</Form.Group>
) : ""}
{ showCocktailFilter ? (
<Form.Group controlId="cocktail-mix">
<Form.Label>Cocktail</Form.Label>
<Form.Check
type="checkbox"
label="Mix"
name="mix"
value={1}
defaultChecked={arg.searchParams.has("mix") ? true : false}
/>
</Form.Group>
) : ""}
<Form.Group controlId="cocktail-mix">
<Form.Label>No Inventory</Form.Label>
<Form.Check
type="checkbox"
label="Show"
name="inventory"
value={1}
defaultChecked={arg.searchParams.has("inventory") ? true : false}
/>
</Form.Group>
</Col>
</Row>
</Form>
);
}
export default DrinkFilter;
+37 -23
View File
@@ -1,36 +1,50 @@
import {LinkContainer} from 'react-router-bootstrap'
import {Container, Nav, Navbar, NavDropdown} from 'react-bootstrap';
import { User } from '@zenstackhq/runtime/models';
import { Link } from '@remix-run/react';
export type HeaderData = {user?: (User)} & {loggedin: boolean};
type Arguments = {data: HeaderData};
function Header(data: Arguments) {
var loggedin = data.data.loggedin;
var username = "";
if(loggedin){
username = data.data.user!.username;
}
function Header() {
return (
<Navbar collapseOnSelect expand="md" className="bg-body-tertiary">
<Navbar collapseOnSelect expand="md" className="bg-body-tertiary mb-3">
<Container>
<LinkContainer to="/">
<Navbar.Brand>K-FRIDGE</Navbar.Brand>
</LinkContainer>
<Navbar.Toggle aria-controls="responsive-navbar-nav" />
<Navbar.Collapse id="responsive-navbar-nav">
<Navbar.Toggle />
<Navbar.Collapse>
<Nav className="me-auto">
<Nav.Link href="#features">Features</Nav.Link>
<Nav.Link href="#pricing">Pricing</Nav.Link>
<NavDropdown title="Dropdown" id="collapsible-nav-dropdown">
<NavDropdown.Item href="#action/3.1">Action</NavDropdown.Item>
<NavDropdown.Item href="#action/3.2">
Another action
</NavDropdown.Item>
<NavDropdown.Item href="#action/3.3">Something</NavDropdown.Item>
<NavDropdown.Divider />
<NavDropdown.Item href="#action/3.4">
Separated link
</NavDropdown.Item>
</NavDropdown>
</Nav>
<Nav>
<Nav.Link href="#deets">More deets</Nav.Link>
<Nav.Link eventKey={2} href="#memes">
Dank memes
</Nav.Link>
<LinkContainer to="/beers">
<Nav.Link>Beers</Nav.Link>
</LinkContainer>
<LinkContainer to="/search">
<Nav.Link>Search</Nav.Link>
</LinkContainer>
<LinkContainer to="/scan">
<Nav.Link>Scan</Nav.Link>
</LinkContainer>
</Nav>
{ data.data.loggedin ? (
<Nav>
<LinkContainer to="/logout">
<Nav.Link>Logout ({username})</Nav.Link>
</LinkContainer>
</Nav>
) : (
<Nav>
<Nav.Link href="https://www.linkedin.com/in/kenneth-van-ewijk/">Kenneth</Nav.Link>
</Nav>
)}
</Navbar.Collapse>
</Container>
</Navbar>
+94
View File
@@ -0,0 +1,94 @@
export default function ClientThemeComponent(){
const getPreferredTheme = () => {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
const setTheme = (theme:string) => {
if (theme === 'auto') {
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
} else {
document.documentElement.setAttribute('data-bs-theme', theme)
}
}
setTheme(getPreferredTheme())
const showActiveTheme = (theme:string, focus = false) => {
const themeSwitcher = document.querySelector('#bd-theme')
if (!themeSwitcher) {
return
}
const themeSwitcherText = document.querySelector('#bd-theme-text')
const activeThemeIcon = document.querySelector('.theme-icon-active use')
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
const svgOfActiveBtn = btnToActive!.querySelector('svg use')!.getAttribute('href')
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
element.classList.remove('active')
element.setAttribute('aria-pressed', 'false')
})
btnToActive!.classList.add('active')
btnToActive!.setAttribute('aria-pressed', 'true')
activeThemeIcon!.setAttribute('href', svgOfActiveBtn!)
const themeSwitcherLabel = `${themeSwitcherText!.textContent} (${btnToActive!.dataset!.bsThemeValue})`
themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
if (focus) {
themeSwitcher.focus()
}
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
setTheme(getPreferredTheme())
})
window.addEventListener('DOMContentLoaded', () => {
showActiveTheme(getPreferredTheme())
document.querySelectorAll('[data-bs-theme-value]')
.forEach(toggle => {
toggle.addEventListener('click', () => {
const theme = toggle.getAttribute('data-bs-theme-value') || "light";
setTheme(theme)
showActiveTheme(theme, true)
})
})
});
return (
<div className="dropdown position-fixed bottom-0 end-0 mb-3 me-3 bd-mode-toggle">
<button className="btn btn-bd-primary py-2 dropdown-toggle d-flex align-items-center" id="bd-theme" type="button" aria-expanded="false" data-bs-toggle="dropdown" aria-label="Toggle theme (light)">
<svg className="bi my-1 theme-icon-active" width="1em" height="1em"><use href="#sun-fill"></use></svg>
<span className="visually-hidden" id="bd-theme-text">Toggle theme</span>
</button>
<ul className="dropdown-menu dropdown-menu-end shadow" aria-labelledby="bd-theme-text">
<li>
<button type="button" className="dropdown-item d-flex align-items-center active" data-bs-theme-value="light" aria-pressed="true">
<svg className="bi me-2 opacity-50" width="1em" height="1em"><use href="#sun-fill"></use></svg>
Light
<svg className="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
</button>
</li>
<li>
<button type="button" className="dropdown-item d-flex align-items-center" data-bs-theme-value="dark" aria-pressed="false">
<svg className="bi me-2 opacity-50" width="1em" height="1em"><use href="#moon-stars-fill"></use></svg>
Dark
<svg className="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
</button>
</li>
<li>
<button type="button" className="dropdown-item d-flex align-items-center" data-bs-theme-value="auto" aria-pressed="false">
<svg className="bi me-2 opacity-50" width="1em" height="1em"><use href="#circle-half"></use></svg>
Auto
<svg className="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
</button>
</li>
</ul>
</div>
);
}