Add filtering, authentication and much more
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user