Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c08a035416 | ||
|
|
0f90972bcb | ||
|
|
36881293ed | ||
|
|
7f7ae8e1ac |
@@ -2,8 +2,6 @@ name: Build dev docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
@@ -84,6 +82,6 @@ jobs:
|
||||
tag_name: ${{ gitea.ref_name }}
|
||||
name: ${{ gitea.ref_name }}
|
||||
body: |
|
||||
"## Docker images"
|
||||
"`gitea.furb.it/${{ gitea.repository }}:${{ gitea.ref_name }}`"
|
||||
"`gitea.furb.it/${{ gitea.repository }}:latest`"
|
||||
## Docker images
|
||||
`gitea.furb.it/${{ gitea.repository }}:${{ gitea.ref_name }}`
|
||||
`gitea.furb.it/${{ gitea.repository }}:latest`
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Build dev docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
# Checkout repository for build
|
||||
- name: Checkout beer-inventory repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ gitea.ref }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
# Install NPM packages
|
||||
- name: Install NPM packages
|
||||
run: npm ci
|
||||
|
||||
# Build project
|
||||
- name: Build project
|
||||
run: |
|
||||
npx zenstack generate
|
||||
npx remix vite:build
|
||||
@@ -1,5 +1,10 @@
|
||||
Beer-inventory project for home use.
|
||||
|
||||
The project has three aspects.
|
||||
- The website, which people can visit to see the current inventory of drinks. They can see in which section a drink is stored.
|
||||
- The Admin website, which is part of the main website but only to logged in admin users. Which allows updating drinks, inventory, and adding or editing data.
|
||||
- The scanner, which is a raspberry pi with touchscreen display and a USB barcode scanner. This is next to the fridge, and people use it to scan a drink to remove it from the inventory.
|
||||
|
||||
Used technology:
|
||||
- remix.run (now known as react router)
|
||||
- prisma
|
||||
|
||||
@@ -17,7 +17,6 @@ function toDate(value: Date | string | null | undefined): Date | null {
|
||||
}
|
||||
|
||||
function DrinkCard(arg:Arguments) {
|
||||
var trimmedDescription = arg.drink.description.length > 120 ? arg.drink.description.substring(0, 120) + "..." : arg.drink.description;
|
||||
var link = "/inventory/";
|
||||
|
||||
switch (arg.drink.type) {
|
||||
@@ -61,10 +60,7 @@ function DrinkCard(arg:Arguments) {
|
||||
}
|
||||
|
||||
const drinkAddedAt = toDate((arg.drink as { addedAt?: Date | string | null }).addedAt ?? null);
|
||||
const latestTimestamp = drinkAddedAt && (!latestActivity || drinkAddedAt.getTime() > latestActivity.getTime())
|
||||
? drinkAddedAt
|
||||
: latestActivity;
|
||||
const isNewlyAdded = latestTimestamp ? (Date.now() - latestTimestamp.getTime()) <= 30 * 24 * 60 * 60 * 1000 : false;
|
||||
const isNewlyAdded = drinkAddedAt ? (Date.now() - drinkAddedAt.getTime()) <= 30 * 24 * 60 * 60 * 1000 : false;
|
||||
const isRecentlyRestocked = latestActivity ? (Date.now() - latestActivity.getTime()) <= 14 * 24 * 60 * 60 * 1000 : false;
|
||||
|
||||
return (
|
||||
@@ -72,11 +68,11 @@ function DrinkCard(arg:Arguments) {
|
||||
<div style={{ position: 'relative', width: '100%', aspectRatio: '1 / 1', overflow: 'hidden' }}>
|
||||
{ (isNewlyAdded || isRecentlyRestocked) ? (
|
||||
<Badge
|
||||
bg={isRecentlyRestocked ? "success" : "primary"}
|
||||
bg={isNewlyAdded ? "primary" : "success"}
|
||||
className='position-absolute top-0 start-0 m-2 text-wrap'
|
||||
style={{ zIndex: 2 }}
|
||||
>
|
||||
{isNewlyAdded && isRecentlyRestocked ? "New · Restocked" : isNewlyAdded ? "New" : "Recently added"}
|
||||
{isNewlyAdded ? "New" : (isRecentlyRestocked ? "Restocked" : "")}
|
||||
</Badge>
|
||||
) : null }
|
||||
<Card.Img
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useSubmit } from '@remix-run/react';
|
||||
import { BeerStyle, Manufacturer, WineStyle } from '@zenstackhq/runtime/models';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Form, Col, Row } from 'react-bootstrap';
|
||||
import { Button, Form, Col, Row } from 'react-bootstrap';
|
||||
|
||||
interface Arguments {
|
||||
beerStyles: BeerStyle[];
|
||||
@@ -20,11 +20,28 @@ function DrinkFilter(arg : Arguments) {
|
||||
const showSodaFilter = arg.searchParams.has("drink", "Soda");
|
||||
const showCocktailFilter = arg.searchParams.has("drink", "Cocktail");
|
||||
|
||||
const form = useRef(null);
|
||||
const form = useRef<HTMLFormElement | null>(null);
|
||||
const drinkTypes = ["Beer", "Wine", "Soda", "Cocktail"];
|
||||
const [beerStyleExpanded, setBeerStyleExpanded] = useState(false);
|
||||
const [wineStyleExpanded, setWineStyleExpanded] = useState(false);
|
||||
|
||||
const submitAfterRender = function(){
|
||||
setTimeout(() => submit(form.current), 10);
|
||||
}
|
||||
const toggleDrinkType = (drinkType: string) => {
|
||||
const params = new URLSearchParams(arg.searchParams.toString());
|
||||
const values = params.getAll("drink");
|
||||
|
||||
if (values.includes(drinkType)) {
|
||||
params.delete("drink");
|
||||
values.filter((value) => value !== drinkType).forEach((value) => params.append("drink", value));
|
||||
} else {
|
||||
params.append("drink", drinkType);
|
||||
}
|
||||
|
||||
if (params.get("abv") === "all") {
|
||||
params.delete("abv");
|
||||
}
|
||||
|
||||
submit(params, { method: "get", replace: true });
|
||||
};
|
||||
|
||||
const handleFormChange = (event: React.ChangeEvent<HTMLFormElement>) => {
|
||||
const formData = new FormData(event.currentTarget);
|
||||
@@ -37,6 +54,8 @@ function DrinkFilter(arg : Arguments) {
|
||||
}
|
||||
|
||||
const abvSelection = arg.searchParams.get("abv") || "all";
|
||||
const selectedBeerStyleCount = arg.searchParams.getAll("beerstyle").length;
|
||||
const selectedWineStyleCount = arg.searchParams.getAll("winestyle").length;
|
||||
const abvOptions = [
|
||||
{ value: "all", label: "All" },
|
||||
{ value: "alcohol-free", label: "Alcohol free" },
|
||||
@@ -57,7 +76,7 @@ function DrinkFilter(arg : Arguments) {
|
||||
/>
|
||||
));
|
||||
|
||||
const sortKey = arg.searchParams.get("sort") || "name";
|
||||
const sortKey = arg.searchParams.get("sort") || "new";
|
||||
|
||||
return (
|
||||
<Form noValidate method="GET" ref={form} className='pe-2' onChange={handleFormChange}>
|
||||
@@ -66,8 +85,8 @@ function DrinkFilter(arg : Arguments) {
|
||||
<Form.Group controlId="sort" className='bg-body-secondary rounded p-3'>
|
||||
<Form.Label className='fw-bold'>Sort</Form.Label>
|
||||
{ [
|
||||
["name", "Name"],
|
||||
["new", "New"],
|
||||
["name", "Name"],
|
||||
["popular", "Popularity"],
|
||||
["abv-asc", "Alcohol (Low -> High)"],
|
||||
["abv-desc", "Alcohol (High -> Low)"],
|
||||
@@ -87,17 +106,24 @@ function DrinkFilter(arg : Arguments) {
|
||||
</Form.Group>
|
||||
<Form.Group controlId="drink-type" className='bg-body-secondary rounded p-3 mt-3'>
|
||||
<Form.Label className='fw-bold'>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}
|
||||
/>
|
||||
<div className='row row-cols-2 g-2'>
|
||||
{drinkTypes.map((key) => (
|
||||
<div className='col' key={key}>
|
||||
<Button
|
||||
type="button"
|
||||
variant={arg.searchParams.has("drink", key) ? "primary" : "outline-primary"}
|
||||
className='w-100 text-start'
|
||||
aria-pressed={arg.searchParams.has("drink", key)}
|
||||
onClick={() => toggleDrinkType(key)}
|
||||
>
|
||||
{key}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{arg.searchParams.getAll("drink").map((drinkValue) => (
|
||||
<input key={drinkValue} type="hidden" name="drink" value={drinkValue} />
|
||||
))}
|
||||
|
||||
</Form.Group>
|
||||
<Form.Group controlId="drink-abv" className='bg-body-secondary rounded p-3 mt-3'>
|
||||
<Form.Label className='fw-bold'>Alcohol Percentage</Form.Label>
|
||||
@@ -119,34 +145,67 @@ function DrinkFilter(arg : Arguments) {
|
||||
{ showBeerFilter ? (
|
||||
<>
|
||||
<Form.Group controlId="beer-style" className='bg-body-secondary rounded p-3 mt-3'>
|
||||
<Form.Label className='fw-bold mt-3'>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}
|
||||
/>
|
||||
))}
|
||||
<div className='d-flex justify-content-between align-items-center'>
|
||||
<Form.Label className='fw-bold mb-0'>Beer style {selectedBeerStyleCount > 0 ? ( <span className="badge text-bg-dark">{selectedBeerStyleCount} selected</span>) : ''}</Form.Label>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline-secondary'
|
||||
size='sm'
|
||||
onClick={() => setBeerStyleExpanded((current) => !current)}
|
||||
>
|
||||
{beerStyleExpanded ? 'Collapse' : 'Expand'}
|
||||
</Button>
|
||||
</div>
|
||||
{beerStyleExpanded ? (
|
||||
<div className='mt-3'>
|
||||
{ arg.beerStyles.map(style => (
|
||||
<Form.Check
|
||||
key={style.id}
|
||||
id={"beerstyle-" + style.name}
|
||||
type="checkbox"
|
||||
label={style.name}
|
||||
name="beerstyle"
|
||||
value={style.id}
|
||||
defaultChecked={arg.searchParams.has("beerstyle", style.id.toString()) ? true : false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-muted mt-3'>Tap expand to show beer styles</div>
|
||||
)}
|
||||
</Form.Group>
|
||||
</>
|
||||
) : ""}
|
||||
{ showWineFilter ? (
|
||||
<Form.Group controlId="wine-style" className='bg-body-secondary rounded p-3 mt-3'>
|
||||
<Form.Label className='fw-bold'>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>
|
||||
<div className='d-flex justify-content-between align-items-center'>
|
||||
<Form.Label className='fw-bold mb-0'>Wine style {selectedWineStyleCount > 0 ? ( <span className="badge text-bg-dark">{selectedWineStyleCount} selected</span>) : ''}</Form.Label>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline-secondary'
|
||||
size='sm'
|
||||
onClick={() => setWineStyleExpanded((current) => !current)}
|
||||
>
|
||||
{wineStyleExpanded ? 'Collapse' : 'Expand'}
|
||||
</Button>
|
||||
</div>
|
||||
{wineStyleExpanded ? (
|
||||
<div className='mt-3'>
|
||||
{ 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}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-muted mt-3'>Tap expand to show wine styles</div>
|
||||
)}
|
||||
</Form.Group>
|
||||
) : ""}
|
||||
{ showSodaFilter ? (
|
||||
<Form.Group controlId="soda-carbonated" className='bg-body-secondary rounded p-3 mt-3'>
|
||||
@@ -154,6 +213,7 @@ function DrinkFilter(arg : Arguments) {
|
||||
<Form.Check
|
||||
type="radio"
|
||||
label="Both"
|
||||
id="carbonated-both"
|
||||
name="carbonated"
|
||||
value={0}
|
||||
defaultChecked={(arg.searchParams.get("carbonated") == "0" || !arg.searchParams.has("carbonated")) ? true : false}
|
||||
@@ -161,6 +221,7 @@ function DrinkFilter(arg : Arguments) {
|
||||
<Form.Check
|
||||
type="radio"
|
||||
label="Sparkling"
|
||||
id="carbonated-sparkling"
|
||||
name="carbonated"
|
||||
value={1}
|
||||
defaultChecked={arg.searchParams.get("carbonated") == "1" ? true : false}
|
||||
@@ -168,6 +229,7 @@ function DrinkFilter(arg : Arguments) {
|
||||
<Form.Check
|
||||
type="radio"
|
||||
label="Non-sparkling"
|
||||
id="carbonated-flat"
|
||||
name="carbonated"
|
||||
value={2}
|
||||
defaultChecked={arg.searchParams.get("carbonated") == "2" ? true : false}
|
||||
|
||||
@@ -97,7 +97,11 @@ function sortNewest(n1:DrinkComposite, n2:DrinkComposite) : number{
|
||||
|
||||
export function sortDrinksFromSearch(searchParams: URLSearchParams, searchResult: DrinkComposite[]) : DrinkComposite[] {
|
||||
|
||||
var callback = sortName;
|
||||
var callback = sortNewest;
|
||||
|
||||
if(searchParams.has("sort", "new")){
|
||||
callback = sortNewest;
|
||||
}
|
||||
|
||||
if(searchParams.has("sort", "name")){
|
||||
callback = sortName;
|
||||
@@ -123,10 +127,6 @@ export function sortDrinksFromSearch(searchParams: URLSearchParams, searchResult
|
||||
callback = sortPopularity;
|
||||
}
|
||||
|
||||
if(searchParams.has("sort", "new")){
|
||||
callback = sortNewest;
|
||||
}
|
||||
|
||||
var sortedArray: DrinkComposite[] = searchResult.sort(callback);
|
||||
|
||||
return sortedArray;
|
||||
|
||||
+78
-1
@@ -8,7 +8,7 @@ import {
|
||||
isRouteErrorResponse,
|
||||
useRouteError
|
||||
} from "@remix-run/react";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import React, { useEffect, useState, type PropsWithChildren } from "react";
|
||||
|
||||
import stylesheet from "bootstrap/dist/css/bootstrap.min.css?url";
|
||||
|
||||
@@ -53,6 +53,45 @@ export default function App() {
|
||||
export function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
|
||||
const [isOnline, setIsOnline] = useState<boolean>(
|
||||
typeof navigator !== "undefined" ? navigator.onLine : true
|
||||
);
|
||||
|
||||
const tryRefresh = async () => {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
const res = await fetch(window.location.href, { method: "GET", cache: "no-store" });
|
||||
if (res && res.ok) {
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (e) {
|
||||
// network still down; ignore, we'll retry on the next interval or when online
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const handleOnline = () => {
|
||||
setIsOnline(true);
|
||||
tryRefresh();
|
||||
};
|
||||
const handleOffline = () => setIsOnline(false);
|
||||
|
||||
window.addEventListener("online", handleOnline);
|
||||
window.addEventListener("offline", handleOffline);
|
||||
|
||||
const hourly = window.setInterval(() => {
|
||||
tryRefresh();
|
||||
}, 15 * 60 * 1000); // Every 15 minutes
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("online", handleOnline);
|
||||
window.removeEventListener("offline", handleOffline);
|
||||
window.clearInterval(hourly);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
return (
|
||||
<Document
|
||||
@@ -64,6 +103,25 @@ export function ErrorBoundary() {
|
||||
<div className="h-100 p-5 text-bg-dark rounded-3">
|
||||
<h1>{error.status}</h1>
|
||||
{error.statusText}
|
||||
<div className="mt-3">
|
||||
<button
|
||||
className="btn btn-primary me-2"
|
||||
onClick={() => {
|
||||
if (typeof window !== "undefined") window.location.reload();
|
||||
}}
|
||||
>
|
||||
Reload page
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={() => {
|
||||
tryRefresh();
|
||||
}}
|
||||
>
|
||||
Try refresh now
|
||||
</button>
|
||||
<div className="mt-2">Status: {isOnline ? "Online" : "Offline"}. Will retry automatically every 15 minutes.</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -84,6 +142,25 @@ export function ErrorBoundary() {
|
||||
<div className="h-100 p-5 text-bg-dark rounded-3">
|
||||
<h1>App Error</h1>
|
||||
{errorMessage}
|
||||
<div className="mt-3">
|
||||
<button
|
||||
className="btn btn-primary me-2"
|
||||
onClick={() => {
|
||||
if (typeof window !== "undefined") window.location.reload();
|
||||
}}
|
||||
>
|
||||
Reload page
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={() => {
|
||||
tryRefresh();
|
||||
}}
|
||||
>
|
||||
Try refresh now
|
||||
</button>
|
||||
<div className="mt-2">Status: {isOnline ? "Online" : "Offline"}. Will retry automatically every 15 minutes.</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -78,11 +78,7 @@ export default function BeersRoute() {
|
||||
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")) {
|
||||
if (key === "sort" && value === "new") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function InventoryLayout() {
|
||||
setCheckouts(newCheckouts);
|
||||
}
|
||||
|
||||
}, 5000);
|
||||
}, 30000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
});
|
||||
|
||||
+40
-14
@@ -1,8 +1,8 @@
|
||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { json, redirect } from "@remix-run/node";
|
||||
import { useActionData, useLoaderData } from "@remix-run/react";
|
||||
import { Form as RemixForm, useActionData, useLoaderData } from "@remix-run/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Col, Container, Form, Row, Table } from "react-bootstrap";
|
||||
import { Col, Container, Form as BootstrapForm, Row, Table } from "react-bootstrap";
|
||||
import timeAgo from "~/utils/datetime";
|
||||
|
||||
import { enhance } from "~/utils/db.server";
|
||||
@@ -16,7 +16,7 @@ export async function loader({
|
||||
return redirect("/");
|
||||
}
|
||||
|
||||
const history = await dbe.history.findMany({select: {id: true, checkoutAt: true, inventoryAfter: true, container: {select: {drink: true}}}, take: 10, orderBy: {checkoutAt: "desc"}});
|
||||
const history = await dbe.history.findMany({select: {id: true, checkoutAt: true, inventoryAfter: true, container: {select: {drink: true}}}, take: 5, orderBy: {checkoutAt: "desc"}});
|
||||
return json({history});
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export async function action({
|
||||
export default function ScanRoute() {
|
||||
const aData = useActionData<typeof action>();
|
||||
const lData = useLoaderData<typeof loader>();
|
||||
const fullscreenBtnRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const [lastScanned, setLastScanned] = useState<{name:string;image:string|null} | null>(null);
|
||||
const [showLastScanned, setShowLastScanned] = useState(false);
|
||||
@@ -76,6 +77,19 @@ export default function ScanRoute() {
|
||||
window.setTimeout(() => inputRef.current?.focus(), 0);
|
||||
};
|
||||
|
||||
const toggleFullscreen = async () => {
|
||||
if (typeof document === "undefined") return;
|
||||
try {
|
||||
if (document.fullscreenElement) {
|
||||
await document.exitFullscreen();
|
||||
} else {
|
||||
await document.documentElement.requestFullscreen();
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore errors (user gesture required in some contexts)
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
focusInput();
|
||||
}, []);
|
||||
@@ -102,6 +116,10 @@ export default function ScanRoute() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
|
||||
setLastScanned(aData.lastScanned);
|
||||
setShowLastScanned(true);
|
||||
|
||||
@@ -116,14 +134,14 @@ export default function ScanRoute() {
|
||||
<Container fluid className="mt-3">
|
||||
<Row>
|
||||
<Col xs={12} md={6} className="mb-3">
|
||||
<h3>History</h3>
|
||||
<div style={{ maxHeight: '70vh', overflowY: 'auto' }}>
|
||||
<h3 onClick={() => fullscreenBtnRef.current?.click()} style={{cursor: 'pointer'}}>History</h3>
|
||||
<div>
|
||||
<Table striped size="sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Drink</th>
|
||||
<th>When</th>
|
||||
<th>Amount left</th>
|
||||
<th># Left</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -139,12 +157,20 @@ export default function ScanRoute() {
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={12} md={6}>
|
||||
<h3>Scan Barcode</h3>
|
||||
<h3 onClick={() => fullscreenBtnRef.current?.click()} style={{cursor: 'pointer'}}>Scan Barcode</h3>
|
||||
<Row className="mb-3">
|
||||
<Col>
|
||||
<Form method="post" action="/scan?index" noValidate>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
{/* Hidden fullscreen toggle button - triggered by clicking the h3 headers */}
|
||||
<button
|
||||
type="button"
|
||||
ref={fullscreenBtnRef}
|
||||
onClick={toggleFullscreen}
|
||||
style={{ display: 'none' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<RemixForm method="post" action="/scan?index" noValidate>
|
||||
<BootstrapForm.Group>
|
||||
<BootstrapForm.Control
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
name="barcode"
|
||||
@@ -154,11 +180,11 @@ export default function ScanRoute() {
|
||||
inputMode="text"
|
||||
style={{ height: '3rem', fontSize: '1.8rem', textAlign: 'center' }}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
<BootstrapForm.Control.Feedback type="invalid">
|
||||
{aData?.error}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</BootstrapForm.Control.Feedback>
|
||||
</BootstrapForm.Group>
|
||||
</RemixForm>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
|
||||
Reference in New Issue
Block a user