diff --git a/deno.jsonc b/deno.jsonc index c6f9b5f..e35491e 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -23,9 +23,12 @@ "yup": "https://esm.sh/yup@1.2.0", "imagekit": "npm:imagekit@4.1.3", "barcode-polyfill": "https://esm.sh/barcode-detector@2.0.3", + + // Tailwind my beloved "tailwindcss": "npm:tailwindcss@3.3.5", "tailwindcss/": "npm:/tailwindcss@3.3.5/", - "tailwindcss/plugin": "npm:/tailwindcss@3.3.5/plugin.js" + "tailwindcss/plugin": "npm:/tailwindcss@3.3.5/plugin.js", + "@tailwindcss/typography": "npm:@tailwindcss/typography" }, "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" }, "exclude": ["**/_fresh/*"] diff --git a/fresh.gen.ts b/fresh.gen.ts index ed673fc..cef5624 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -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"; @@ -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"; @@ -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": @@ -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, diff --git a/islands/events/editing/showtimeSelector.tsx b/islands/events/editing/showtimeSelector.tsx new file mode 100644 index 0000000..171e1f0 --- /dev/null +++ b/islands/events/editing/showtimeSelector.tsx @@ -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[]; +}) => { + 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 ( +
+ +
+ ); +}; + +export default ShowtimeSelector; diff --git a/islands/events/scanning.tsx b/islands/events/scanning.tsx index 99cb248..f8b1429 100644 --- a/islands/events/scanning.tsx +++ b/islands/events/scanning.tsx @@ -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(null); - const [initialized, setInitialized] = useState(false); +export default function Scanner({ + className, + eventID, +}: { + className?: string; + eventID: string; +}) { + const error = useSignal(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"], @@ -27,9 +42,9 @@ 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 { @@ -37,74 +52,173 @@ export default function Scanner({ className }: { className?: string }) { 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); + + 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(); @@ -114,7 +228,7 @@ export default function Scanner({ className }: { className?: string }) { requestAnimationFrame(loop); }; } catch (e) { - setError(e.message); + error.value = e.message; return; } })(); @@ -130,9 +244,15 @@ export default function Scanner({ className }: { className?: string }) { muted={true} className="border-none" /> - {error} -
+ {error.value} +
+
+ Bring a ticket code into view +
); diff --git a/islands/events/viewing/register.tsx b/islands/events/viewing/register.tsx index cd25218..9de51b1 100644 --- a/islands/events/viewing/register.tsx +++ b/islands/events/viewing/register.tsx @@ -17,6 +17,7 @@ import Ticket from "@/islands/components/peices/ticket.tsx"; import { acquired, getTicketID } from "@/utils/tickets.ts"; import { EventRegisterError } from "@/utils/event/register.ts"; import { RegisterErrors } from "../components/registerErrors.tsx"; +import SelectShowTime from "./selectShowTime.tsx" export default function EventRegister({ eventID, @@ -181,70 +182,7 @@ export default function EventRegister({ } }; - const SelectShowTime = () => { - return ( - <> - - (changeOpen.value = false)} - isOpen={changeOpen.value} - className="md:!max-w-md" - > -

Change

-
- {showTimes.map((time) => ( - - ))} -
- -
- - ); - }; + const Popover = () => { return ( @@ -265,7 +203,7 @@ export default function EventRegister({
{page.value == 0 ? ( <> - + {acquired(user, eventID, showTime.value!) ? ( <>
diff --git a/islands/events/viewing/selectShowTime.tsx b/islands/events/viewing/selectShowTime.tsx new file mode 100644 index 0000000..f98adec --- /dev/null +++ b/islands/events/viewing/selectShowTime.tsx @@ -0,0 +1,114 @@ +import { ShowTime } from "@/utils/db/kv.types.ts"; +import { Signal } from "@preact/signals"; +import Popup from "@/components/popup.tsx"; +import { fmtDate, fmtHour, fmtTime } from "@/utils/dates.ts"; + +const SelectShowTime = ({ + showTimes, + showTime, + changeOpen, + setShowTime, + all = false, +}: { + showTimes: Partial[]; + changeOpen: Signal; + showTime: Signal | string; + setShowTime?: (showTime: string) => void; + all?: boolean; +}) => { + const selectedTime = showTimes + .find((time) => + typeof showTime == "string" + ? time.id == showTime + : time.id == showTime.value, + ) + return ( + <> + + (changeOpen.value = false)} + isOpen={changeOpen.value} + className="md:!max-w-md" + > +

Change

+
+ {showTimes.map((time) => ( + + ))} + {/* All event times button */} + {all && ( + + )} +
+ +
+ + ); +}; + +export default SelectShowTime; diff --git a/islands/loginForm.tsx b/islands/loginForm.tsx index 6fa63ae..480ba4f 100644 --- a/islands/loginForm.tsx +++ b/islands/loginForm.tsx @@ -110,8 +110,7 @@ const LoginForm = ({ attending }: { attending: boolean }) => { name="email" value={email} onChange={(e) => - //@ts-expect-error deno moment - setEmail(e.target!.value) + setEmail(e.currentTarget.value) } /> @@ -163,8 +162,7 @@ const LoginForm = ({ attending }: { attending: boolean }) => { pattern="[0-9]*" value={code} onInput={(e) => - //@ts-expect-error deno moment (e has incorrect types and idk what the correct types are) - updateCode(e.target!.value) + updateCode(e.currentTarget.value) } ref={codeRef} onBlur={() => setFocused(false)} diff --git a/islands/tickets/filters.tsx b/islands/tickets/filters.tsx index 7967726..164ca23 100644 --- a/islands/tickets/filters.tsx +++ b/islands/tickets/filters.tsx @@ -12,7 +12,7 @@ export default function TicketsFilters({ }) { return ( { e.preventDefault(); diff --git a/routes/api/events/fetch.ts b/routes/api/events/fetch.ts index 3d864cf..00e3893 100644 --- a/routes/api/events/fetch.ts +++ b/routes/api/events/fetch.ts @@ -1,5 +1,6 @@ import { Handlers } from "$fresh/server.ts"; import { Event, getUser, kv, Ticket } from "@/utils/db/kv.ts"; +import { isUUID } from "@/utils/db/misc.ts"; export const handler: Handlers = { async POST(req) { @@ -25,6 +26,23 @@ export const handler: Handlers = { }: { eventID: string; ticketID: string; showtimeID?: string } = await req.json(); + const ticketSplit = ticketID.split("_"); + + for (const segment of ticketSplit) { + if (!isUUID(segment)) { + return new Response( + JSON.stringify({ + error: { + message: "Invalid ticket ID", + }, + }), + { + status: 400, + }, + ); + } + } + const event = await kv.get(["event", eventID]); if (event.value == undefined) { diff --git a/routes/events/[id]/(no-layout)/index.tsx b/routes/events/[id]/(no-layout)/index.tsx index 86a0d06..6f58182 100644 --- a/routes/events/[id]/(no-layout)/index.tsx +++ b/routes/events/[id]/(no-layout)/index.tsx @@ -77,9 +77,9 @@ export default defineRoute((req, ctx: RouteContext) => {

{event.venue && ( -
+
-

{event.venue}

+

{event.venue}

)}
@@ -140,9 +140,6 @@ export default defineRoute((req, ctx: RouteContext) => { return st; }); - const truncate = (str: string, len: number) => - str.length > len ? str.slice(0, len) + "..." : str; - return ( <> diff --git a/routes/events/[id]/scanning.tsx b/routes/events/[id]/scanning.tsx index 6de5139..550e1eb 100644 --- a/routes/events/[id]/scanning.tsx +++ b/routes/events/[id]/scanning.tsx @@ -14,9 +14,14 @@ export default defineRoute((req, ctx: RouteContext) => { } return ( -
+
- + + +
); }); diff --git a/routes/events/[id]/tickets.tsx b/routes/events/[id]/tickets.tsx index 4939eea..9b7aabd 100644 --- a/routes/events/[id]/tickets.tsx +++ b/routes/events/[id]/tickets.tsx @@ -12,12 +12,14 @@ import DotsVertical from "$tabler/dots-vertical.tsx"; import { fmtDate, fmtHour } from "@/utils/dates.ts"; import TicketsFilters from "@/islands/tickets/filters.tsx"; import { signal } from "@preact/signals"; +import ShowtimeSelector from "@/islands/events/editing/showtimeSelector.tsx"; export default defineRoute((req, ctx: RouteContext) => { const { event, eventID, user } = ctx.state.data; const url = new URL(req.url); const queryValue = url.searchParams.get("q"); + const showTimeID = url.searchParams.get("id") ?? event.showTimes[0].id; let sortValue = parseInt(url.searchParams.get("s") ?? "0"); if (isNaN(sortValue) || sortValue > 4 || sortValue < 0) { @@ -34,7 +36,13 @@ export default defineRoute((req, ctx: RouteContext) => { return (
+ + +