AI Changes: offline handling, fullscreen handling
This commit is contained in:
@@ -2,8 +2,6 @@ name: Build dev docker image
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -84,6 +82,6 @@ jobs:
|
|||||||
tag_name: ${{ gitea.ref_name }}
|
tag_name: ${{ gitea.ref_name }}
|
||||||
name: ${{ gitea.ref_name }}
|
name: ${{ gitea.ref_name }}
|
||||||
body: |
|
body: |
|
||||||
"## Docker images"
|
## Docker images
|
||||||
"`gitea.furb.it/${{ gitea.repository }}:${{ gitea.ref_name }}`"
|
`gitea.furb.it/${{ gitea.repository }}:${{ gitea.ref_name }}`
|
||||||
"`gitea.furb.it/${{ gitea.repository }}:latest`"
|
`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
|
||||||
+78
-1
@@ -8,7 +8,7 @@ import {
|
|||||||
isRouteErrorResponse,
|
isRouteErrorResponse,
|
||||||
useRouteError
|
useRouteError
|
||||||
} from "@remix-run/react";
|
} 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";
|
import stylesheet from "bootstrap/dist/css/bootstrap.min.css?url";
|
||||||
|
|
||||||
@@ -53,6 +53,45 @@ export default function App() {
|
|||||||
export function ErrorBoundary() {
|
export function ErrorBoundary() {
|
||||||
const error = useRouteError();
|
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();
|
||||||
|
}, 60 * 60 * 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("online", handleOnline);
|
||||||
|
window.removeEventListener("offline", handleOffline);
|
||||||
|
window.clearInterval(hourly);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (isRouteErrorResponse(error)) {
|
if (isRouteErrorResponse(error)) {
|
||||||
return (
|
return (
|
||||||
<Document
|
<Document
|
||||||
@@ -64,6 +103,25 @@ export function ErrorBoundary() {
|
|||||||
<div className="h-100 p-5 text-bg-dark rounded-3">
|
<div className="h-100 p-5 text-bg-dark rounded-3">
|
||||||
<h1>{error.status}</h1>
|
<h1>{error.status}</h1>
|
||||||
{error.statusText}
|
{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 hour.</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -84,6 +142,25 @@ export function ErrorBoundary() {
|
|||||||
<div className="h-100 p-5 text-bg-dark rounded-3">
|
<div className="h-100 p-5 text-bg-dark rounded-3">
|
||||||
<h1>App Error</h1>
|
<h1>App Error</h1>
|
||||||
{errorMessage}
|
{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 hour.</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export async function action({
|
|||||||
export default function ScanRoute() {
|
export default function ScanRoute() {
|
||||||
const aData = useActionData<typeof action>();
|
const aData = useActionData<typeof action>();
|
||||||
const lData = useLoaderData<typeof loader>();
|
const lData = useLoaderData<typeof loader>();
|
||||||
|
const fullscreenBtnRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
const [lastScanned, setLastScanned] = useState<{name:string;image:string|null} | null>(null);
|
const [lastScanned, setLastScanned] = useState<{name:string;image:string|null} | null>(null);
|
||||||
const [showLastScanned, setShowLastScanned] = useState(false);
|
const [showLastScanned, setShowLastScanned] = useState(false);
|
||||||
@@ -76,6 +77,19 @@ export default function ScanRoute() {
|
|||||||
window.setTimeout(() => inputRef.current?.focus(), 0);
|
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(() => {
|
useEffect(() => {
|
||||||
focusInput();
|
focusInput();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -116,8 +130,8 @@ export default function ScanRoute() {
|
|||||||
<Container fluid className="mt-3">
|
<Container fluid className="mt-3">
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={12} md={6} className="mb-3">
|
<Col xs={12} md={6} className="mb-3">
|
||||||
<h3>History</h3>
|
<h3 onClick={() => fullscreenBtnRef.current?.click()} style={{cursor: 'pointer'}}>History</h3>
|
||||||
<div style={{ maxHeight: '70vh', overflowY: 'auto' }}>
|
<div>
|
||||||
<Table striped size="sm">
|
<Table striped size="sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -139,9 +153,16 @@ export default function ScanRoute() {
|
|||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} md={6}>
|
<Col xs={12} md={6}>
|
||||||
<h3>Scan Barcode</h3>
|
<h3 onClick={() => fullscreenBtnRef.current?.click()} style={{cursor: 'pointer'}}>Scan Barcode</h3>
|
||||||
<Row className="mb-3">
|
<Row className="mb-3">
|
||||||
<Col>
|
<Col>
|
||||||
|
{/* Hidden fullscreen toggle button - triggered by clicking the h3 headers */}
|
||||||
|
<button
|
||||||
|
ref={fullscreenBtnRef}
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
<Form method="post" action="/scan?index" noValidate>
|
<Form method="post" action="/scan?index" noValidate>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
|||||||
Reference in New Issue
Block a user