From a3468b1381619f72372e759853d2b559ad43e466 Mon Sep 17 00:00:00 2001 From: Darguima Date: Fri, 22 Dec 2023 16:51:45 +0000 Subject: [PATCH 01/23] Staff Badge Component --- components/Badge/index.tsx | 58 +++++++++++++++++++--------------- layout/Staff/Badges/Badges.tsx | 23 +++++++------- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/components/Badge/index.tsx b/components/Badge/index.tsx index de649bd9..4285989a 100644 --- a/components/Badge/index.tsx +++ b/components/Badge/index.tsx @@ -1,15 +1,17 @@ import Link from "next/link"; -import { ReactEventHandler, useState } from "react"; +import { AllHTMLAttributes, ReactEventHandler, useState } from "react"; -interface BadgeProps { +interface BadgeProps extends Omit, "id"|"name"> { name: string; id: string | number; avatar: string; tokens: string | number; - owned: boolean; + owned?: boolean; + disableLink?: boolean; + disableOwnedHighlight?: boolean; } -export default function Badge({ name, id, avatar, tokens, owned }: BadgeProps) { +const Badge: React.FC = ({ name, id, avatar, tokens, owned, disableLink = false, disableOwnedHighlight = false, ...rest }) => { const [badgeLoaded, setBadgeLoaded] = useState(false); const [fallbackRan, setFallbackRan] = useState(false) @@ -26,32 +28,38 @@ export default function Badge({ name, id, avatar, tokens, owned }: BadgeProps) { }; return ( - -
- - {!badgeLoaded && } - - {name} setBadgeLoaded(true)} - onError={imageOnError} - /> -
- -
-
{name}
-
{tokens} 💰
-
- + +
+ {!badgeLoaded && } + + {name} setBadgeLoaded(true)} + onError={imageOnError} + /> +
+ +
+
{name}
+
{tokens} 💰
+
+ + ); } +export default Badge; + const BadgeSkeleton = () => { return ( -
+
); }; diff --git a/layout/Staff/Badges/Badges.tsx b/layout/Staff/Badges/Badges.tsx index fc372715..03a6b3f3 100644 --- a/layout/Staff/Badges/Badges.tsx +++ b/layout/Staff/Badges/Badges.tsx @@ -8,6 +8,7 @@ import Layout from "@components/Layout"; import ErrorMessage from "@components/ErrorMessage"; import Filter from "@components/BadgeFilter"; import QRScanner, { FEEDBACK } from "@components/QRScanner"; +import Badge from "@components/Badge"; function Badges() { const [allBadges, updateAllBadges] = useState([]); @@ -115,17 +116,17 @@ function Badges() { badge.name.toLowerCase().includes(searchInput.toLowerCase()) ) .map((badge, index) => ( -
handleBadgeSelected(badge)} - > - {badge.name} -
-
{badge.name}
-
{badge.tokens} 💰
-
-
+ handleBadgeSelected(badge)} + /> ))}
From 5e7637106a14a8ab3d52c9e319ab39e1c13c0b98 Mon Sep 17 00:00:00 2001 From: Darguima Date: Fri, 22 Dec 2023 16:59:14 +0000 Subject: [PATCH 02/23] Add go to top button --- layout/Staff/Badges/Badges.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/layout/Staff/Badges/Badges.tsx b/layout/Staff/Badges/Badges.tsx index 03a6b3f3..cea9ea10 100644 --- a/layout/Staff/Badges/Badges.tsx +++ b/layout/Staff/Badges/Badges.tsx @@ -9,6 +9,7 @@ import ErrorMessage from "@components/ErrorMessage"; import Filter from "@components/BadgeFilter"; import QRScanner, { FEEDBACK } from "@components/QRScanner"; import Badge from "@components/Badge"; +import GoToTop from "@components/GoToTop"; function Badges() { const [allBadges, updateAllBadges] = useState([]); @@ -131,7 +132,9 @@ function Badges() {
)} + {error && } + ); } From a827b99b2bb7b5e73b97c8a2079d909f3cc82b46 Mon Sep 17 00:00:00 2001 From: Darguima Date: Fri, 22 Dec 2023 17:01:15 +0000 Subject: [PATCH 03/23] Ran formatter --- components/Badge/index.tsx | 34 +++++++++++++------ .../components/Challenges/Challenge/index.tsx | 2 +- layout/Home/components/Speakers/index.jsx | 2 +- layout/Staff/Badges/Badges.tsx | 22 ++++++------ 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/components/Badge/index.tsx b/components/Badge/index.tsx index 4285989a..a0a1fc18 100644 --- a/components/Badge/index.tsx +++ b/components/Badge/index.tsx @@ -1,7 +1,8 @@ import Link from "next/link"; import { AllHTMLAttributes, ReactEventHandler, useState } from "react"; -interface BadgeProps extends Omit, "id"|"name"> { +interface BadgeProps + extends Omit, "id" | "name"> { name: string; id: string | number; avatar: string; @@ -11,31 +12,42 @@ interface BadgeProps extends Omit, "id"|"name" disableOwnedHighlight?: boolean; } -const Badge: React.FC = ({ name, id, avatar, tokens, owned, disableLink = false, disableOwnedHighlight = false, ...rest }) => { +const Badge: React.FC = ({ + name, + id, + avatar, + tokens, + owned, + disableLink = false, + disableOwnedHighlight = false, + ...rest +}) => { const [badgeLoaded, setBadgeLoaded] = useState(false); - const [fallbackRan, setFallbackRan] = useState(false) + const [fallbackRan, setFallbackRan] = useState(false); const imageOnError: ReactEventHandler = (e) => { // prevent infinite loop fallback if (fallbackRan) { setBadgeLoaded(true); - return + return; } setBadgeLoaded(false); e.currentTarget.src = "/images/badges/badge-not-found.svg"; - setFallbackRan(true) + setFallbackRan(true); }; return (
- +
{!badgeLoaded && } @@ -54,12 +66,12 @@ const Badge: React.FC = ({ name, id, avatar, tokens, owned, disableL
); -} +}; export default Badge; const BadgeSkeleton = () => { return ( -
+
); }; diff --git a/layout/Challenges/components/Challenges/Challenge/index.tsx b/layout/Challenges/components/Challenges/Challenge/index.tsx index 6371e868..2d07dc56 100644 --- a/layout/Challenges/components/Challenges/Challenge/index.tsx +++ b/layout/Challenges/components/Challenges/Challenge/index.tsx @@ -7,7 +7,7 @@ function Action({ text, url }) {
); diff --git a/layout/Home/components/Speakers/index.jsx b/layout/Home/components/Speakers/index.jsx index b22937f0..6ec61eb6 100644 --- a/layout/Home/components/Speakers/index.jsx +++ b/layout/Home/components/Speakers/index.jsx @@ -16,7 +16,7 @@ export default function Speakers() {
From 9a7df66ffae2c66f2b5a0fa492d7dc60a735658f Mon Sep 17 00:00:00 2001 From: Darguima Date: Fri, 22 Dec 2023 17:16:27 +0000 Subject: [PATCH 04/23] small fix --- components/Badge/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/Badge/index.tsx b/components/Badge/index.tsx index a0a1fc18..9d13874e 100644 --- a/components/Badge/index.tsx +++ b/components/Badge/index.tsx @@ -25,6 +25,8 @@ const Badge: React.FC = ({ const [badgeLoaded, setBadgeLoaded] = useState(false); const [fallbackRan, setFallbackRan] = useState(false); + const highlightBadge = owned || !disableOwnedHighlight || !badgeLoaded; + const imageOnError: ReactEventHandler = (e) => { // prevent infinite loop fallback if (fallbackRan) { @@ -40,9 +42,7 @@ const Badge: React.FC = ({ return (
Date: Fri, 22 Dec 2023 17:26:07 +0000 Subject: [PATCH 05/23] fix types --- components/Badge/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Badge/index.tsx b/components/Badge/index.tsx index 9d13874e..0b8c03bb 100644 --- a/components/Badge/index.tsx +++ b/components/Badge/index.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { AllHTMLAttributes, ReactEventHandler, useState } from "react"; interface BadgeProps - extends Omit, "id" | "name"> { + extends Omit, "id" | "name" | "type"> { name: string; id: string | number; avatar: string; From 39ebd3d178bb0801c08dc3ac9a51dc883072479d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1rio=20Guimar=C3=A3es?= Date: Sat, 13 Jan 2024 15:19:27 +0000 Subject: [PATCH 06/23] converted to ts --- .../{index.jsx => index.tsx} | 83 +++++++++++-------- components/QRScanner/{index.jsx => index.tsx} | 19 ++++- 2 files changed, 65 insertions(+), 37 deletions(-) rename components/QRScanner/BarebonesQRScanner/{index.jsx => index.tsx} (64%) rename components/QRScanner/{index.jsx => index.tsx} (81%) diff --git a/components/QRScanner/BarebonesQRScanner/index.jsx b/components/QRScanner/BarebonesQRScanner/index.tsx similarity index 64% rename from components/QRScanner/BarebonesQRScanner/index.jsx rename to components/QRScanner/BarebonesQRScanner/index.tsx index d6b5682e..4181c958 100644 --- a/components/QRScanner/BarebonesQRScanner/index.jsx +++ b/components/QRScanner/BarebonesQRScanner/index.tsx @@ -1,11 +1,17 @@ import { useEffect, useRef, useState } from "react"; import jsQR from "jsqr"; +import { Point } from "jsqr/dist/locator"; -function BarebonesQRScanner({ handleCode, pauseRef }) { - const canvasRef = useRef(null); - const videoRef = useRef(null); - const wrapperRef = useRef(null); - const animationFrameRef = useRef(); +interface Props { + handleCode: (uuid: string) => void; + pauseRef: React.MutableRefObject; +} + +const BarebonesQRScanner: React.FC = ({ handleCode, pauseRef }) => { + const videoRef = useRef(null); + const canvasRef = useRef(null); + const wrapperRef = useRef(null); + const animationFrameRef = useRef(); const [error, setError] = useState(""); useEffect(() => { @@ -13,51 +19,36 @@ function BarebonesQRScanner({ handleCode, pauseRef }) { navigator.mediaDevices .getUserMedia({ video: { facingMode: "environment" } }) - .then(function (stream) { + .then((stream) => { //to prevent AbortError on Firefox in strict mode if (!video.srcObject) { setError(""); video.srcObject = stream; - video.setAttribute("playsinline", true); // required to tell iOS safari we don't want fullscreen + video.setAttribute("playsinline", "true"); // required to tell iOS safari we don't want fullscreen video.play(); animationFrameRef.current = requestAnimationFrame(tick); } }) .catch((e) => { - setError( - "We couldn't access your camera. Check if your camera is being used by another app and if you gave us permission to use it." - ); - video.srcObject = undefined; + if (!video.srcObject) { + setError( + "We couldn't access your camera. Check if your camera is being used by another app and if you gave us permission to use it." + ); + video.srcObject = undefined; + } }); return () => { if (video.srcObject) { - video.srcObject.getTracks().forEach((track) => track.stop()); + (video.srcObject as MediaStream) + .getTracks() + .forEach((track) => track.stop()); video.srcObject = undefined; } }; }, []); - function drawLine(canvas, begin, end, color) { - canvas.beginPath(); - canvas.moveTo(begin.x, begin.y); - canvas.lineTo(end.x, end.y); - canvas.lineWidth = 4; - canvas.strokeStyle = color; - canvas.stroke(); - } - - function parseURL(url) { - try { - const url_obj = new URL(url); - if (url_obj.host !== process.env.NEXT_PUBLIC_QRCODE_HOST) return null; - return url_obj.pathname.split("/").at(-1); - } catch { - return null; - } - } - - function tick() { + const tick = () => { const video = videoRef.current; const canvas = canvasRef.current; @@ -116,7 +107,7 @@ function BarebonesQRScanner({ handleCode, pauseRef }) { } } animationFrameRef.current = requestAnimationFrame(tick); - } + }; return ( <> @@ -131,6 +122,30 @@ function BarebonesQRScanner({ handleCode, pauseRef }) {
); -} +}; export default BarebonesQRScanner; + +const drawLine = ( + canvas: CanvasRenderingContext2D, + begin: Point, + end: Point, + color: string +) => { + canvas.beginPath(); + canvas.moveTo(begin.x, begin.y); + canvas.lineTo(end.x, end.y); + canvas.lineWidth = 4; + canvas.strokeStyle = color; + canvas.stroke(); +}; + +const parseURL = (url: string) => { + try { + const url_obj = new URL(url); + if (url_obj.host !== process.env.NEXT_PUBLIC_QRCODE_HOST) return null; + return url_obj.pathname.split("/").at(-1); + } catch { + return null; + } +}; diff --git a/components/QRScanner/index.jsx b/components/QRScanner/index.tsx similarity index 81% rename from components/QRScanner/index.jsx rename to components/QRScanner/index.tsx index 5d05d4be..8a7e8af5 100644 --- a/components/QRScanner/index.jsx +++ b/components/QRScanner/index.tsx @@ -24,7 +24,20 @@ export const FEEDBACK = { }, }; -function QRScanner({ +interface Props { + handleCode: (uuid: string) => void; + pauseRef: React.MutableRefObject; + text: string; + feedback: { + message: string; + color: string; + }; + showScanner: boolean; + setScanner: (show: boolean) => void; + removeClose?: boolean; +} + +const QRScanner: React.FC = ({ handleCode, pauseRef, text, @@ -32,7 +45,7 @@ function QRScanner({ showScanner, setScanner, removeClose, -}) { +}) => { if (!showScanner) { return null; } @@ -60,6 +73,6 @@ function QRScanner({ )}
); -} +}; export default QRScanner; From 033e7dfa21549ff04e78ad73047969c0ea1cef01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1rio=20Guimar=C3=A3es?= Date: Mon, 29 Jan 2024 10:34:17 +0000 Subject: [PATCH 07/23] Rewrite BarebonesQRScanner to better performance --- .../QRScanner/BarebonesQRScanner/index.tsx | 155 ++++++++---------- 1 file changed, 69 insertions(+), 86 deletions(-) diff --git a/components/QRScanner/BarebonesQRScanner/index.tsx b/components/QRScanner/BarebonesQRScanner/index.tsx index 4181c958..01196b50 100644 --- a/components/QRScanner/BarebonesQRScanner/index.tsx +++ b/components/QRScanner/BarebonesQRScanner/index.tsx @@ -1,6 +1,10 @@ -import { useEffect, useRef, useState } from "react"; +import { useRef, useState, useEffect } from "react"; import jsQR from "jsqr"; -import { Point } from "jsqr/dist/locator"; + +const CAPTURE_OPTIONS = { + audio: false, + video: { facingMode: "environment" }, +}; interface Props { handleCode: (uuid: string) => void; @@ -10,49 +14,49 @@ interface Props { const BarebonesQRScanner: React.FC = ({ handleCode, pauseRef }) => { const videoRef = useRef(null); const canvasRef = useRef(null); - const wrapperRef = useRef(null); const animationFrameRef = useRef(); - const [error, setError] = useState(""); + + const [error, setError] = useState(""); useEffect(() => { const video = videoRef.current; - navigator.mediaDevices - .getUserMedia({ video: { facingMode: "environment" } }) - .then((stream) => { - //to prevent AbortError on Firefox in strict mode - if (!video.srcObject) { - setError(""); - video.srcObject = stream; - video.setAttribute("playsinline", "true"); // required to tell iOS safari we don't want fullscreen - video.play(); - animationFrameRef.current = requestAnimationFrame(tick); - } - }) - .catch((e) => { - if (!video.srcObject) { - setError( - "We couldn't access your camera. Check if your camera is being used by another app and if you gave us permission to use it." - ); - video.srcObject = undefined; - } - }); + if (!video?.srcObject) { + navigator.mediaDevices + .getUserMedia(CAPTURE_OPTIONS) + .then((stream) => { + if (!video?.srcObject) { + setError(""); + video.srcObject = stream; + video.setAttribute("playsinline", "true"); // required to tell iOS safari we don't want fullscreen + video.play(); + animationFrameRef.current = + requestAnimationFrame(drawQRBoundingBox); + } + }) + .catch((err) => { + if (!video?.srcObject && err instanceof DOMException) { + setError( + "We couldn't access your camera. Check if your camera is being used by another app and if you gave us permission to use it." + ); + } + }); + } return () => { - if (video.srcObject) { - (video.srcObject as MediaStream) - .getTracks() - .forEach((track) => track.stop()); - video.srcObject = undefined; + if (video && video.srcObject) { + (video.srcObject as MediaStream).getTracks().forEach((track) => { + track.stop(); + }); } }; }, []); - const tick = () => { - const video = videoRef.current; - const canvas = canvasRef.current; + const drawQRBoundingBox = () => { + const video = videoRef?.current; + const canvas = canvasRef?.current; - if (!canvas) { + if (!video || !canvas) { cancelAnimationFrame(animationFrameRef.current); return null; } @@ -60,41 +64,31 @@ const BarebonesQRScanner: React.FC = ({ handleCode, pauseRef }) => { const canvas2D = canvas.getContext("2d"); if (video.readyState === video.HAVE_ENOUGH_DATA) { - canvas.hidden = false; - canvas.height = video.videoHeight; canvas.width = video.videoWidth; + + // Will use the canvas to get the video image data, and pass it to jsQR, but will then clear the canvas to just draw the bounding box canvas2D.drawImage(video, 0, 0, canvas.width, canvas.height); - var imageData = canvas2D.getImageData(0, 0, canvas.width, canvas.height); - var code = jsQR(imageData.data, imageData.width, imageData.height, { + const imageData = canvas2D.getImageData(0, 0, canvas.width, canvas.height); + const code = jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: "dontInvert", }); + canvas2D.clearRect(0, 0, canvas.width, canvas.height); if (code) { - drawLine( - canvas2D, - code.location.topLeftCorner, - code.location.topRightCorner, - "#78f400" - ); - drawLine( - canvas2D, - code.location.topRightCorner, - code.location.bottomRightCorner, - "#78f400" - ); - drawLine( - canvas2D, - code.location.bottomRightCorner, - code.location.bottomLeftCorner, - "#78f400" - ); - drawLine( - canvas2D, - code.location.bottomLeftCorner, - code.location.topLeftCorner, - "#78f400" - ); + const {topLeftCorner, topRightCorner, bottomLeftCorner, bottomRightCorner} = code.location; + + canvas2D.beginPath(); + + canvas2D.moveTo(topLeftCorner.x, topLeftCorner.y); + canvas2D.lineTo(topRightCorner.x, topRightCorner.y); + canvas2D.lineTo(bottomRightCorner.x, bottomRightCorner.y); + canvas2D.lineTo(bottomLeftCorner.x, bottomLeftCorner.y); + canvas2D.lineTo(topLeftCorner.x, topLeftCorner.y); + + canvas2D.lineWidth = 4; + canvas2D.strokeStyle = "#78f400"; + canvas2D.stroke(); if (!pauseRef.current) { const uuid = parseURL(code.data); @@ -106,40 +100,29 @@ const BarebonesQRScanner: React.FC = ({ handleCode, pauseRef }) => { } } } - animationFrameRef.current = requestAnimationFrame(tick); + + animationFrameRef.current = requestAnimationFrame(drawQRBoundingBox); }; return ( - <> -