Skip to content

Commit

Permalink
feat: Basic web interface functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
pklaschka committed Sep 6, 2024
1 parent 9e38bab commit 024c68f
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 14 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ $$ n \in [r_{min}, r]; n = r_{min} + t \mod (r - r_{min}) $$
- [ ] Log generated ASNs to the file system
- [x] Connection to a database
- [x] REST API
- [ ] Visual Web Interface
- [x] Visual Web Interface
- [ ] CLI
- [ ] Bump counter to avoid collissions after restoring backups (where ASNs
could have been generated after the time of the backup)
Expand All @@ -61,3 +61,4 @@ $$ n \in [r_{min}, r]; n = r_{min} + t \mod (r - r_{min}) $$
- [ ] Publish on JSR
- [ ] Publish on Docker Hub
- [x] Publish on GitHub Container Registry
- [ ] UI for quickly searching for ASNs in a DMS
3 changes: 2 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"$cli/": "./lib/cli/",
"@hono/hono": "jsr:@hono/hono@^4.5.11",
"@std/datetime": "jsr:@std/datetime@^0.225.2",
"@collinhacks/zod": "npm:zod@^3.23.8"
"@collinhacks/zod": "npm:zod@^3.23.8",
"@metafloor/bwip-js": "npm:bwip-js@^4.5.1"
},
"compilerOptions": {
"jsx": "precompile",
Expand Down
6 changes: 6 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion lib/common/asn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import { CONFIG } from "$common/config.ts";
import { performAtomicTransaction } from "$common/db.ts";
import { ensureFileContent, getCounterPath } from "$common/path.ts";

export interface ASNData {
asn: string;
namespace: number;
prefix: string;
counter: number;
metadata: Record<string, unknown>;
}

function getCurrentNamespace(): number {
const date = Date.now();
const range = getRange();
Expand All @@ -19,7 +27,7 @@ function getRange() {
* The ASN is composed of a prefix, a namespace, and a counter.
* @returns a new ASN (Alphanumeric Serial Number)
*/
export async function generateASN(metadata: Record<string, unknown> = {}) {
export async function generateASN(metadata: Record<string, unknown> = {}): Promise<ASNData> {
metadata = { ...metadata, generatedAt: new Date().toISOString() };
const namespace = getCurrentNamespace();
let counter = 0;
Expand Down Expand Up @@ -81,3 +89,9 @@ export function getFormatDescription(): string {
` The user defined namespace can be used for pre-printed ASN barcodes and the like.\n` +
`(3) Counter, starting from 001, incrementing with each new ASN in the namespace.`;
}

export function validateASN(asn: string): boolean {
return new RegExp(
`^${CONFIG.ASN_PREFIX}(\\d{${`${CONFIG.ASN_NAMESPACE_RANGE}`.length}})(\\d{3})\\d*$`,
).test(asn);
}
2 changes: 1 addition & 1 deletion lib/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface Config {

const configSchema = z.object({
PORT: z.number({ coerce: true }).default(8080),
ASN_PREFIX: z.string().min(1),
ASN_PREFIX: z.string().min(1).max(10).regex(/^[A-Z]+$/),
ASN_NAMESPACE_RANGE: z.number({ coerce: true }),
});

Expand Down
12 changes: 12 additions & 0 deletions lib/http/barcode-svg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as bwip from "@metafloor/bwip-js";

export function createBarcodeSVG(data: string, embedded = false): string {
return bwip.toSVG({
bcid: 'code128', // Barcode type
text: data, // Text to encode
scale: 3, // 3x scaling factor
height: 10, // Bar height, in millimeters
includetext: !embedded, // Show human-readable text
textxalign: 'center', // Always good to set this
});
}
34 changes: 32 additions & 2 deletions lib/http/mod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { Hono } from "@hono/hono";
import { logger } from "@hono/hono/logger";
import { jsxRenderer } from "@hono/hono/jsx-renderer";
import { serveStatic } from "@hono/hono/deno";
import { generateASN } from "$common/mod.ts";

import { generateASN, getFormatDescription, validateASN } from "$common/mod.ts";
import metadata from "$/deno.json" with { type: "json" };

import { Wrapper } from "./ui/wrapper.tsx";
import { Wrapper } from "$http/ui/wrapper.tsx";
import { IndexPage } from "$http/ui/index.tsx";
import { createBarcodeSVG } from "$http/barcode-svg.ts";

export const httpApp = new Hono();

Expand All @@ -15,8 +17,36 @@ httpApp.get(
"/about",
(c) => c.text(`${metadata.name} v${metadata.version} is running!`),
);
httpApp.get("/format", (c) => c.text(getFormatDescription()));
httpApp.get("/json", async (c) => c.json(await generateASN()));
httpApp.get("/static/*", serveStatic({ root: "." }));

httpApp.get("/svg/:asn", (c) => {
const requestedASN = c.req.param("asn");

if (!requestedASN) {
return c.text("No ASN provided", 400);
}

if (!validateASN(requestedASN)) {
return c.text("Invalid ASN provided", 400);
}

const barcode = createBarcodeSVG(requestedASN, !!c.req.query("embed"));

return c.body(barcode ?? "", 200, {
"Cache-Control": "no-cache",
"Content-Type": "image/svg+xml",
});
});

httpApp.get("/svg", (c) => {
const barcode = createBarcodeSVG("0123456789", !!c.req.query("embed"));
return c.body(barcode ?? "", 200, {
"Cache-Control": "no-cache",
"Content-Type": "image/svg+xml",
});
});

httpApp.use("*", jsxRenderer(Wrapper));
httpApp.use("/", async (c) => await c.render(<IndexPage />));
38 changes: 36 additions & 2 deletions lib/http/ui/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
export function IndexPage() {
return <h1>Test</h1>
import { css, cx } from "@hono/hono/css";
import { generateASN } from "$common/asn.ts";

const hideOnPrint = css`
@media print {
display: none;
}
`

const mainClass = css`
display: flex;
flex-direction: column;
gap: 0rem;
justify-content: center;
align-items: center;
`

export async function IndexPage() {
const asn = await generateASN();
const script = {__html: `globalThis.asn = ${JSON.stringify(asn)};`};
return <>
<script dangerouslySetInnerHTML={script}></script>
<header class={cx(hideOnPrint)}>
<button onclick={'globalThis.copy()'}>Copy</button>
<button onclick={'globalThis.location.reload()'}>Reload</button>
<button onclick={'globalThis.print()'}>Print</button>
<a href={`/svg/${asn.asn}`} download>Download</a>
</header>
<main class={mainClass}>
<img src={`/svg/${asn.asn}?embed=true`} alt="Barcode" />
<p>{asn.asn}</p>
</main>
<footer class={cx(hideOnPrint)}>
<a href="/about">About</a>
</footer>
</>
}
35 changes: 30 additions & 5 deletions lib/http/ui/wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,54 @@ background: var(--primary-color);
display: grid;
place-items: center;
align-content: center;
`
margin: 0;
padding: 1rem;
@media print {
background: transparent;
height: 100%;
align-content: stretch;
place-items: stretch;
}
`;

const mainClass = css`
background: white;
font-family: var(--font-family);
`
width: clamp(0px, 100%, 20rem);
overflow-x: hidden;
padding: 1rem;
@media print {
width: 100%;
height: 100%;
border: 1px solid var(--primary-color);
}
`;

export function Wrapper({ children }: { children?: Child }) {
return (
<html>
<head>
<title>Wrapper</title>
<link rel="stylesheet" href="/static/theme.css" />
<Style>{css`
<Style>
{css`
* {
box-sizing: border-box;
}
`}</Style>
html, body {
height: 100%;
}
`}
</Style>
</head>
<body class={cx(bodyClass)}>
<main class={mainClass}>
{children}
{children}
</main>
<script src="/static/main.js"></script>
</body>
</html>
);
Expand Down
36 changes: 36 additions & 0 deletions static/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* The ASN rendered by the current page
* @type {import("$common/asn.ts").ASNData | undefined}
*/
const asn = globalThis.asn;

init()
.then(() => console.log("✅ Interactive Elements initialized"))
.catch((err) =>
console.error(
"🛑 An error occured while initializing Interactive Elements",
err
)
);

async function init() {
console.log("⌚️ Initializing Interactive Elements...");
if (!asn) {
console.warn("ASN not found");
return;
}

console.debug("ASN found:", asn);

globalThis.copy = () => {
navigator.clipboard.writeText(asn.asn).then(() => {
console.log("✅ Copied ASN to clipboard");
});
};

globalThis.downloadBarcode = () => {
// TODO: Implement barcode download
}

await Promise.resolve();
}
10 changes: 9 additions & 1 deletion static/theme.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
:root {
--primary-color: #452897;
--font-family: 'Roboto', Arial, sans-serif;
}
}

body {
font-family: var(--font-family);
}

a {
color: var(--primary-color);
}

0 comments on commit 024c68f

Please sign in to comment.