Skip to content

Commit

Permalink
start scanning + better ticket showtime UI
Browse files Browse the repository at this point in the history
Co-Authored-By: Bloxs <[email protected]>
  • Loading branch information
quick007 and Blocksnmore committed Dec 3, 2023
1 parent b4f3c7b commit 2ffd220
Show file tree
Hide file tree
Showing 13 changed files with 388 additions and 146 deletions.
5 changes: 4 additions & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@
"yup": "https://esm.sh/[email protected]",
"imagekit": "npm:[email protected]",
"barcode-polyfill": "https://esm.sh/[email protected]",

// Tailwind my beloved
"tailwindcss": "npm:[email protected]",
"tailwindcss/": "npm:/[email protected]/",
"tailwindcss/plugin": "npm:/[email protected]/plugin.js"
"tailwindcss/plugin": "npm:/[email protected]/plugin.js",
"@tailwindcss/typography": "npm:@tailwindcss/typography"
},
"compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" },
"exclude": ["**/_fresh/*"]
Expand Down
6 changes: 6 additions & 0 deletions fresh.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import * as $events_creation_zero from "./islands/events/creation/zero.tsx";
import * as $events_editing_delete from "./islands/events/editing/delete.tsx";
import * as $events_editing_images from "./islands/events/editing/images.tsx";
import * as $events_editing_settings from "./islands/events/editing/settings.tsx";
import * as $events_editing_showtimeSelector from "./islands/events/editing/showtimeSelector.tsx";
import * as $events_editing_showtimesettings from "./islands/events/editing/showtimesettings.tsx";
import * as $events_editing_ticketSettings from "./islands/events/editing/ticketSettings.tsx";
import * as $events_list_filters from "./islands/events/list/filters.tsx";
Expand All @@ -73,6 +74,7 @@ import * as $events_teams_invite from "./islands/events/teams/invite.tsx";
import * as $events_teams_manage from "./islands/events/teams/manage.tsx";
import * as $events_viewing_availability from "./islands/events/viewing/availability.tsx";
import * as $events_viewing_register from "./islands/events/viewing/register.tsx";
import * as $events_viewing_selectShowTime from "./islands/events/viewing/selectShowTime.tsx";
import * as $events_viewing_showtimes from "./islands/events/viewing/showtimes.tsx";
import * as $loginForm from "./islands/loginForm.tsx";
import * as $queueManagement from "./islands/queueManagement.tsx";
Expand Down Expand Up @@ -155,6 +157,8 @@ const manifest = {
"./islands/events/editing/delete.tsx": $events_editing_delete,
"./islands/events/editing/images.tsx": $events_editing_images,
"./islands/events/editing/settings.tsx": $events_editing_settings,
"./islands/events/editing/showtimeSelector.tsx":
$events_editing_showtimeSelector,
"./islands/events/editing/showtimesettings.tsx":
$events_editing_showtimesettings,
"./islands/events/editing/ticketSettings.tsx":
Expand All @@ -166,6 +170,8 @@ const manifest = {
"./islands/events/teams/manage.tsx": $events_teams_manage,
"./islands/events/viewing/availability.tsx": $events_viewing_availability,
"./islands/events/viewing/register.tsx": $events_viewing_register,
"./islands/events/viewing/selectShowTime.tsx":
$events_viewing_selectShowTime,
"./islands/events/viewing/showtimes.tsx": $events_viewing_showtimes,
"./islands/loginForm.tsx": $loginForm,
"./islands/queueManagement.tsx": $queueManagement,
Expand Down
36 changes: 36 additions & 0 deletions islands/events/editing/showtimeSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useSignal } from "@preact/signals";
import SelectShowTime from "../viewing/selectShowTime.tsx";
import { ShowTime } from "@/utils/db/kv.types.ts";

const ShowtimeSelector = ({
defaultShowTime,
showTimes,
}: {
defaultShowTime: string;
showTimes: Partial<ShowTime>[];
}) => {
const changeOpen = useSignal(false);

const setShowTime = (showTime: string) => {
if (showTime == defaultShowTime) return;
const url = new URL(window.location.href);

url.searchParams.set("id", showTime);

location.href = url.toString();
};

return (
<div class="mt-8 mb-2 ml-auto">
<SelectShowTime
all={true}
changeOpen={changeOpen}
showTime={defaultShowTime}
showTimes={showTimes}
setShowTime={setShowTime}
/>
</div>
);
};

export default ShowtimeSelector;
250 changes: 185 additions & 65 deletions islands/events/scanning.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
import { useEffect, useState } from "preact/hooks";
import { useEffect } from "preact/hooks";
import { IS_BROWSER } from "$fresh/runtime.ts";
// Currently causes issues, hopefully it's fixed soon
//import { BarcodeDetector } from "npm:barcode-detector";
import { BarcodeDetector, DetectedBarcode } from "npm:barcode-detector";
import { Ticket } from "@/utils/db/kv.types.ts";
import { useSignal } from "@preact/signals";

export default function Scanner({ className }: { className?: string }) {
const [error, setError] = useState<string | null>(null);
const [initialized, setInitialized] = useState(false);
export default function Scanner({
className,
eventID,
}: {
className?: string;
eventID: string;
}) {
const error = useSignal<string | null>(null);
const isInitialized = useSignal(false);
const currentTicket = useSignal<
{ code: string; status: "invalid" | "loading", ticketData:null } | {
code: string;
status: "used" | "valid";
ticketData: Ticket;
} | null
>(null);

useEffect(() => {
(async () => {
if (!IS_BROWSER) return;
if (initialized) return;
setInitialized(true);
if (isInitialized.value) return;
isInitialized.value = true;
const canvas = document.getElementById("scanui") as HTMLCanvasElement;
const barcodeReaderAPI = window["BarcodeDetector"] ?? BarcodeDetector;
if (barcodeReaderAPI == null) {
return setError(
"BarcodeDetector API is required but not supported on your device! Please try another browser.",
);
error.value =
"BarcodeDetector API is required but not supported on your device! Please try another browser.";
return;
}
const reader = new barcodeReaderAPI({
formats: ["qr_code"],
Expand All @@ -27,84 +42,183 @@ export default function Scanner({ className }: { className?: string }) {
willReadFrequently: true,
});
if (ctx == null) {
return setError(
"2D HTML Canvas is required but not supported on your device! Please try another browser.",
);
error.value =
"2D HTML Canvas is required but not supported on your device! Please try another browser.";
return;
}

try {
const devices = await navigator.mediaDevices.getUserMedia({
video: true,
});
const video = document.getElementById("camera") as HTMLVideoElement;
const infoText = document.getElementById("scantext") as HTMLDivElement;
let lastStr = infoText.innerText;

const updateStringIfChanged = (str: string) => {
if (lastStr != str) {
lastStr = str;
infoText.innerText = str;
}
};

if (!video) return;

video.srcObject = devices;
video.onloadedmetadata = () => {
video.play();
const container = document.getElementById("scale-factor")!;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;

const checkedCodes: string[] = [];
const checkedCodes: Map<
string,
| { status: "loading" | "invalid"; checkedAt: number }
| {
status: "valid" | "used";
ticketData: Ticket;
checkedAt: number;
}
> = new Map();

const lookForBarcodes = async () => {
// const codes = await reader.detect(video);
// if (codes.length > 0) {
// for (const code of codes) {
// if (checkedCodes.includes(code.rawValue)) continue;
// checkedCodes.push(code.rawValue);
// console.log(code);
// }
// }
setInterval(() => {
for (const [code, codeData] of checkedCodes) {
const timeSinceScan = Date.now() - codeData.checkedAt;

if (codeData.status == "loading" && timeSinceScan > 5 * 1000) {
checkedCodes.delete(code);
}

if (timeSinceScan > 15 * 1000) {
checkedCodes.delete(code);
}
}
}, 5 * 1000);

const fetchCodeInfo = async (code: string) => {
const res = await fetch(`/api/events/fetch`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ ticketID: code, eventID: eventID }),
});
const data = await (res.json() as Promise<Ticket>);

if (res.status == 400) {
checkedCodes.set(code, {
status: "invalid",
checkedAt: Date.now(),
});
} else {
checkedCodes.set(code, {
status: data.hasBeenUsed ? "used" : "valid",
ticketData: data,
checkedAt: Date.now(),
});
}
};

// TODO: @quick007 Fix this because our brains are cooking in their own fluids - Bloxs
// No.
console.log(
"Video:",
video.videoWidth,
video.videoHeight,
const lookForBarcodes = async () => {
const codes = await reader.detect(video);
if (codes.length > 0) {
const largestCode: {
size: number;
code: DetectedBarcode | null;
} = {
size: 0,
code: null,
};

"Canvas:",
canvas.width,
canvas.height,
for (const code of codes) {
if (
code.boundingBox.width * code.boundingBox.height >
largestCode.size
) {
largestCode.size = code.boundingBox.width *
code.boundingBox.height;
largestCode.code = code;
}
}

"Meth:",
(video.videoWidth - canvas.width) / 2,
(video.videoHeight - canvas.height) / 2,
if (largestCode.code != undefined) {
const code = largestCode.code;

"Meth 2 electric boogaloo:",
video.videoWidth - (video.videoWidth - canvas.width) / 2,
video.videoHeight - (video.videoHeight - canvas.height) / 2,
if (!checkedCodes.has(code.rawValue)) {
checkedCodes.set(code.rawValue, {
status: "loading",
checkedAt: Date.now(),
});

"Aspects: ",
(video.videoWidth -
(video.videoWidth - canvas.width) / 2 -
(video.videoWidth - canvas.width) / 2) /
(video.videoHeight -
(video.videoHeight - canvas.height) / 2 -
(video.videoHeight - canvas.height) / 2),
fetchCodeInfo(code.rawValue);
}

canvas.width / canvas.height,
);
const codeData = checkedCodes.get(code.rawValue)!;

const loop = () => {
ctx.drawImage(
video,
const ticketObj = {
code: code.rawValue,
status: codeData.status,
// @ts-expect-error types
ticketData: Object.hasOwn(codeData, "ticketData") ? codeData.ticketData : null,
};

(video.videoWidth - canvas.width) / 2,
(video.videoHeight - canvas.height) / 2,
if (currentTicket.value != ticketObj) {
currentTicket.value = ticketObj;
}

video.videoWidth - (video.videoWidth - canvas.width) / 2,
video.videoHeight - (video.videoHeight - canvas.height) / 2,
ctx.fillStyle = ctx.strokeStyle = {
invalid: "red",
loading: "gray",
valid: "green",
used: "orange",
}[codeData.status];

0,
0,
canvas.width,
canvas.height,
);
ctx.lineWidth = 10;
ctx.moveTo(code.cornerPoints[0].x, code.cornerPoints[0].y);
ctx.beginPath();

let lowestY = 0;
let leftmostX = canvas.width;
let rightmostX = 0;

for (const point of code.cornerPoints) {
lowestY = Math.max(lowestY, point.y);
leftmostX = Math.min(leftmostX, point.x);
rightmostX = Math.max(rightmostX, point.x);
ctx.lineTo(point.x, point.y);
}

ctx.closePath();
ctx.stroke();

switch (codeData.status) {
case "loading": {
updateStringIfChanged("Loading...");
break;
}

case "invalid": {
updateStringIfChanged("Invalid code!");
break;
}

case "valid": {
updateStringIfChanged("Scan ticket");
break;
}

case "used": {
updateStringIfChanged("Ticket already used!");
break;
}
}
} else {
currentTicket.value = null;
}
}
};

const loop = () => {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

lookForBarcodes();

Expand All @@ -114,7 +228,7 @@ export default function Scanner({ className }: { className?: string }) {
requestAnimationFrame(loop);
};
} catch (e) {
setError(e.message);
error.value = e.message;
return;
}
})();
Expand All @@ -130,9 +244,15 @@ export default function Scanner({ className }: { className?: string }) {
muted={true}
className="border-none"
/>
{error}
<div id="scale-factor" className="h-[30rem] w-[30rem]">
{error.value}
<div class="flex flex-col items-center max-w-full relative">
<canvas id="scanui" className={className}></canvas>
<div
class="absolute rounded-md bg-black/50 backdrop-blur px-4 py-2 text-white bottom-4"
id="scantext"
>
Bring a ticket code into view
</div>
</div>
</>
);
Expand Down
Loading

0 comments on commit 2ffd220

Please sign in to comment.