diff --git a/.DS_Store b/.DS_Store index 01ce626..5f9ac6e 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/frontend/package.json b/frontend/package.json index 76c6a07..62fb0ca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "eject": "react-scripts eject" }, "dependencies": { + "gsap": "^3.13.0", "motion": "^12.23.22", "react": "^19.1.1", "react-dom": "^19.1.1" diff --git a/frontend/src/index.css b/frontend/src/index.css index d86eaf1..b2ff5db 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -32,3 +32,59 @@ section[id] { .animate-fade-in { animation: fadeIn 1s ease-out forwards; } + +@keyframes float { + 0%, 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-10px); + } +} + +.animate-float { + animation: float 3s ease-in-out infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@keyframes floatAndSpin { + 0%, 100% { + transform: translateY(0px) rotate(0deg); + } + 50% { + transform: translateY(-10px) rotate(180deg); + } +} + +@keyframes floatAndSpinReverse { + 0%, 100% { + transform: translateY(0px) rotate(0deg); + } + 50% { + transform: translateY(-8px) rotate(-180deg); + } +} + +.animate-spin-slow { + animation: spin 8s linear infinite; +} + +.animate-spin-reverse { + animation: spin 10s linear infinite reverse; +} + +.animate-float-spin { + animation: floatAndSpin 6s ease-in-out infinite; +} + +.animate-float-spin-reverse { + animation: floatAndSpinReverse 7s ease-in-out infinite; +} diff --git a/frontend/src/types/images.d.ts b/frontend/src/types/images.d.ts index c9ad21f..e33fbf4 100644 --- a/frontend/src/types/images.d.ts +++ b/frontend/src/types/images.d.ts @@ -19,8 +19,10 @@ declare module '*.gif' { } declare module '*.svg' { - const value: string; - export default value; + import React = require('react'); + export const ReactComponent: React.FC>; + const src: string; + export default src; } declare module '*.webp' { diff --git a/frontend/src/ui/common/assets/faq/grid.png b/frontend/src/ui/common/assets/faq/grid.png new file mode 100644 index 0000000..6d3a924 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/grid.png differ diff --git a/frontend/src/ui/common/assets/faq/passport.png b/frontend/src/ui/common/assets/faq/passport.png new file mode 100644 index 0000000..9f0766b Binary files /dev/null and b/frontend/src/ui/common/assets/faq/passport.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_eight.png b/frontend/src/ui/common/assets/faq/stamps/stamp_eight.png new file mode 100644 index 0000000..e13b0a2 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_eight.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_eleven.png b/frontend/src/ui/common/assets/faq/stamps/stamp_eleven.png new file mode 100644 index 0000000..f48777b Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_eleven.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_five.png b/frontend/src/ui/common/assets/faq/stamps/stamp_five.png new file mode 100644 index 0000000..88bc526 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_five.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_four.png b/frontend/src/ui/common/assets/faq/stamps/stamp_four.png new file mode 100644 index 0000000..10d575f Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_four.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_nine.png b/frontend/src/ui/common/assets/faq/stamps/stamp_nine.png new file mode 100644 index 0000000..4ed8303 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_nine.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_one.png b/frontend/src/ui/common/assets/faq/stamps/stamp_one.png new file mode 100644 index 0000000..c505adb Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_one.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_seven.png b/frontend/src/ui/common/assets/faq/stamps/stamp_seven.png new file mode 100644 index 0000000..7fac7b8 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_seven.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_six.png b/frontend/src/ui/common/assets/faq/stamps/stamp_six.png new file mode 100644 index 0000000..ca006e9 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_six.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_ten.png b/frontend/src/ui/common/assets/faq/stamps/stamp_ten.png new file mode 100644 index 0000000..8d5be14 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_ten.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_three.png b/frontend/src/ui/common/assets/faq/stamps/stamp_three.png new file mode 100644 index 0000000..e395a94 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_three.png differ diff --git a/frontend/src/ui/common/assets/faq/stamps/stamp_two.png b/frontend/src/ui/common/assets/faq/stamps/stamp_two.png new file mode 100644 index 0000000..88d6875 Binary files /dev/null and b/frontend/src/ui/common/assets/faq/stamps/stamp_two.png differ diff --git a/frontend/src/ui/common/assets/header/HeaderRope.png b/frontend/src/ui/common/assets/header/HeaderRope.png new file mode 100644 index 0000000..ecda57f Binary files /dev/null and b/frontend/src/ui/common/assets/header/HeaderRope.png differ diff --git a/frontend/src/ui/common/assets/header/MobileHeader.png b/frontend/src/ui/common/assets/header/MobileHeader.png new file mode 100644 index 0000000..a75649f Binary files /dev/null and b/frontend/src/ui/common/assets/header/MobileHeader.png differ diff --git a/frontend/src/ui/common/assets/header/Patch.png b/frontend/src/ui/common/assets/header/Patch.png new file mode 100644 index 0000000..50be755 Binary files /dev/null and b/frontend/src/ui/common/assets/header/Patch.png differ diff --git a/frontend/src/ui/common/assets/header/RegisterButton.png b/frontend/src/ui/common/assets/header/RegisterButton.png new file mode 100644 index 0000000..70b02b1 Binary files /dev/null and b/frontend/src/ui/common/assets/header/RegisterButton.png differ diff --git a/frontend/src/ui/common/assets/header/basicTree.svg b/frontend/src/ui/common/assets/header/basicTree.svg new file mode 100644 index 0000000..b4e4a53 --- /dev/null +++ b/frontend/src/ui/common/assets/header/basicTree.svg @@ -0,0 +1,21 @@ + + + + diff --git a/frontend/src/ui/common/assets/header/line.svg b/frontend/src/ui/common/assets/header/line.svg new file mode 100644 index 0000000..7e6c971 --- /dev/null +++ b/frontend/src/ui/common/assets/header/line.svg @@ -0,0 +1,21 @@ + + + + + + + diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1249.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1249.webp index a59713b..be70c6a 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1249.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1249.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1302.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1302.webp index ac952da..4281d3f 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1302.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1302.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1566.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1566.webp index a51f8bd..fa9a6a7 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1566.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1566.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1624.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1624.webp index f4bbe56..f8dc544 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1624.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1624.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1709.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1709.webp index a0a8f57..dacf64b 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1709.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1709.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1738.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1738.webp index 55b43c2..119bcb9 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1738.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1738.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1834.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1834.webp index 47c7be5..e3aa243 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1834.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1834.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1249.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1249.webp deleted file mode 100644 index 5b12c97..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1249.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1302.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1302.webp deleted file mode 100644 index fc748bf..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1302.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1326.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1326.webp deleted file mode 100644 index aaf8946..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1326.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1353.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1353.webp deleted file mode 100644 index 3558d73..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1353.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1372.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1372.webp deleted file mode 100644 index 4628efe..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1372.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1382.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1382.webp deleted file mode 100644 index 0abf24b..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1382.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1566.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1566.webp deleted file mode 100644 index 439a15c..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1566.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1749.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1749.webp deleted file mode 100644 index a1db172..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1749.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/sponsors/AmericanFidelity.png b/frontend/src/ui/common/assets/sponsors/AmericanFidelity.png new file mode 100644 index 0000000..6be138a Binary files /dev/null and b/frontend/src/ui/common/assets/sponsors/AmericanFidelity.png differ diff --git a/frontend/src/ui/common/assets/sponsors/HorizontalRedBull.svg b/frontend/src/ui/common/assets/sponsors/HorizontalRedBull.svg new file mode 100644 index 0000000..dbd04cb --- /dev/null +++ b/frontend/src/ui/common/assets/sponsors/HorizontalRedBull.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/ui/common/assets/sponsors/PureButtons.png b/frontend/src/ui/common/assets/sponsors/PureButtons.png new file mode 100644 index 0000000..09d1a1a Binary files /dev/null and b/frontend/src/ui/common/assets/sponsors/PureButtons.png differ diff --git a/frontend/src/ui/common/assets/sponsors/TomLove.png b/frontend/src/ui/common/assets/sponsors/TomLove.png new file mode 100644 index 0000000..f281c5a Binary files /dev/null and b/frontend/src/ui/common/assets/sponsors/TomLove.png differ diff --git a/frontend/src/ui/common/assets/sponsors/VerticalRedBull.svg b/frontend/src/ui/common/assets/sponsors/VerticalRedBull.svg new file mode 100644 index 0000000..96cc08c --- /dev/null +++ b/frontend/src/ui/common/assets/sponsors/VerticalRedBull.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/ui/pages/faq/index.tsx b/frontend/src/ui/pages/faq/index.tsx new file mode 100644 index 0000000..9c2b66b --- /dev/null +++ b/frontend/src/ui/pages/faq/index.tsx @@ -0,0 +1,342 @@ +// FAQPage.tsx +import React from "react"; +import gridImg from "../../common/assets/faq/grid.png"; +import passportImg from "../../common/assets/faq/passport.png"; +import SelfieCapture from "./section/SelfieCapture"; + +import stampOne from "../../common/assets/faq/stamps/stamp_one.png"; +import stampTwo from "../../common/assets/faq/stamps/stamp_two.png"; +import stampThree from "../../common/assets/faq/stamps/stamp_three.png"; +import stampFour from "../../common/assets/faq/stamps/stamp_four.png"; +import stampFive from "../../common/assets/faq/stamps/stamp_five.png"; +import stampSix from "../../common/assets/faq/stamps/stamp_six.png"; +import stampSeven from "../../common/assets/faq/stamps/stamp_seven.png"; +import stampEight from "../../common/assets/faq/stamps/stamp_eight.png"; +import stampNine from "../../common/assets/faq/stamps/stamp_nine.png"; +import stampTen from "../../common/assets/faq/stamps/stamp_ten.png"; +import stampEleven from "../../common/assets/faq/stamps/stamp_eleven.png"; + +// Mapping of stamp keys to images +const stampImages = { + stamp_one: stampOne, + stamp_two: stampTwo, + stamp_three: stampThree, + stamp_four: stampFour, + stamp_five: stampFive, + stamp_six: stampSix, + stamp_seven: stampSeven, + stamp_eight: stampEight, + stamp_nine: stampNine, + stamp_ten: stampTen, + stamp_eleven: stampEleven, +} as const; + +// Stamp position adjustments +const stampTranslations = { + stamp_one: "-translate-y-1", + stamp_two: "-translate-y-1", + stamp_three: "-translate-y-1", + stamp_four: "translate-x-0.5 translate-y-1", + stamp_five: "translate-x-0.5 -translate-y-2", + stamp_six: "-translate-y-2", + stamp_seven: "translate-x-0.5 -translate-y-2", + stamp_eight: "-translate-y-2", + stamp_nine: "-translate-y-1", + stamp_ten: "-translate-y-1", + stamp_eleven: "-translate-y-1", +} as const; + +type StampKey = keyof typeof stampImages; + +type Cell = { + id: string; + text: string; + stamp?: StampKey; +}; + +// FAQ cells data +const cells: Cell[] = [ + { + id: "c1", + text: + "A hack is something that is jury-rigged inelegantly but effectively, usually as a temporary solution to a problem. Like duct taping a hole in a sinking boat to keep it afloat.", + stamp: "stamp_one", + }, + { id: "c2", text: "Admissions is completely free for all students!", stamp: "stamp_two" }, + { id: "c3", text: "At this time, we will not be providing travel reimbursements.", stamp: "stamp_three" }, + { + id: "c4", + text: + "We will supply food for Saturday's lunch, dinner, and Sunday's breakfast with plenty of snacks and drinks throughout. All free of charge!", + stamp: "stamp_four", + }, + { + id: "c5", + text: + "No experience is needed. Whether you're a coder, an artist, or a writer, you'll get to work with various mentors, attend workshops, interact with companies, and learn alongside fellow participants.", + stamp: "stamp_five", + }, + { + id: "c6", + text: + "We encourage everyone to work with a team! Teams may contain up to 4 people. We will also be offering a team-building session at the beginning of the hacking period.", + stamp: "stamp_six", + }, + { + id: "c7", + text: + "You should bring a laptop, chargers, toiletries, a change of clothes, sleeping bag, pillow, and anything else you would need for an overnight weekend. Keep in mind that Hacklahoma will last for 24hrs.", + stamp: "stamp_seven", + }, + { + id: "c8", + text: + "Hacklahoma welcomes students from all backgrounds and values the importance of a safe and all-inclusive space. Anyone attending must adhere to the MLH Code of Conduct.", + stamp: "stamp_eight", + }, + { + id: "c9", + text: + "Any student over the age of 18 can participate, regardless of major, background, or skill level.", + stamp: "stamp_nine", + }, + { + id: "c10", + text: + "No, you cannot work or copy past projects. You can brainstorm ideas and collect whatever software and tools you need, as long as the project is completely new.", + stamp: "stamp_ten", + }, + { id: "c11", text: "No, you're not confined here. Feel free to go home and get some rest, but be back in time for judging!", stamp: "stamp_eleven" }, + { id: "c12", text: "If your question wasn't answered, please feel free to contact us via Instagram, Twitter, Facebook or send us an email to hacklahoma@ou.edu" }, +]; + +type StampState = "shown" | "hiding" | "hidden"; + +const FAQPage: React.FC = () => { + const [firstName, setFirstName] = React.useState(""); + const [lastName, setLastName] = React.useState(""); + const [school, setSchool] = React.useState(""); + const [signature, setSignature] = React.useState(""); + + // Stamp state per cell (only cells with stamp start as "shown") + const [stampStates, setStampStates] = React.useState>(() => { + const init: Record = {}; + for (const c of cells) init[c.id] = c.stamp ? "shown" : "hidden"; + return init; + }); + + const dismissStamp = (id: string) => { + setStampStates((prev) => { + if (prev[id] !== "shown") return prev; + return { ...prev, [id]: "hiding" }; + }); + + window.setTimeout(() => { + setStampStates((prev) => ({ ...prev, [id]: "hidden" })); + }, 450); + }; + + const fields = [ + { label: "first name:", placeholder: "type something..." }, + { label: "last name:", placeholder: "type something..." }, + { label: "school:", placeholder: "type something..." }, + ]; + + return ( +
+
+
+ {/* LEFT PAGE */} +
+
+ + + {/* NAME FIELDS */} +
+
+ {fields[0].label} + setFirstName(e.target.value)} + className={` + flex-1 ml-2 bg-transparent border-b border-gray-400 + focus:outline-none focus:border-gray-600 + text-[7px] sm:text-[10px] lg:text-[9px] xl:text-xs + leading-none py-0 placeholder-gray-500 + ${firstName ? "text-black font-bold" : "text-gray-500"} + `} + /> +
+ +
+ {fields[1].label} + setLastName(e.target.value)} + className={` + flex-1 ml-2 bg-transparent border-b border-gray-400 + focus:outline-none focus:border-gray-600 + text-[7px] sm:text-[10px] lg:text-[9px] xl:text-xs + leading-none py-0 placeholder-gray-500 + ${lastName ? "text-black font-bold" : "text-gray-500"} + `} + /> +
+ +
+ {fields[2].label} + setSchool(e.target.value)} + className={` + flex-1 ml-2 bg-transparent border-b border-gray-400 + focus:outline-none focus:border-gray-600 + text-[7px] sm:text-[10px] lg:text-[9px] xl:text-xs + leading-none py-0 placeholder-gray-500 + ${school ? "text-black font-bold" : "text-gray-500"} + `} + /> +
+
+ + {/* SIGNATURE */} +
+
+ X: + setSignature(e.target.value)} + className={` + flex-1 ml-2 bg-transparent border-b border-transparent + focus:outline-none focus:border-transparent + text-[7px] sm:text-[12px] lg:text-[14px] xl:text-lg + leading-none py-0 placeholder-gray-500 + ${signature ? "text-black font-bold" : "text-gray-500"} + `} + /> +
+
+ + {/* SELFIE */} +
+ +
+
+
+ + {/* SPINE (xl+) */} +
+
+
+ + {/* RIGHT PAGE */} +
+
+ + + {/* grid */} +
+ {cells.map((cell) => { + const hasStamp = !!cell.stamp; + const state = stampStates[cell.id] ?? "hidden"; + const stampImage = cell.stamp ? stampImages[cell.stamp] : null; + const stampTranslation = cell.stamp ? stampTranslations[cell.stamp] : ""; + + return ( +
+ {/* Text underneath */} +
+

+ {cell.text} +

+
+ + {/* Stamp overlay fills square + fades away */} + {hasStamp && state !== "hidden" && ( + + )} +
+ ); + })} +
+
+
+
+
+
+ ); +}; + +export default FAQPage; diff --git a/frontend/src/ui/pages/faq/section/SelfieCapture.tsx b/frontend/src/ui/pages/faq/section/SelfieCapture.tsx new file mode 100644 index 0000000..5bd5fa7 --- /dev/null +++ b/frontend/src/ui/pages/faq/section/SelfieCapture.tsx @@ -0,0 +1,298 @@ +// SelfieCapture.tsx +import React, { useEffect, useRef, useState } from "react"; + +/* ---------- IndexedDB helpers ---------- */ + +function openDB(): Promise { + return new Promise((resolve, reject) => { + const req = indexedDB.open("selfie-store", 1); + req.onupgradeneeded = () => { + const db = req.result; + if (!db.objectStoreNames.contains("images")) db.createObjectStore("images"); + }; + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); +} + +async function saveBlobToIDB(key: string, blob: Blob) { + const db = await openDB(); + return new Promise((resolve, reject) => { + try { + const tx = db.transaction("images", "readwrite"); + const store = tx.objectStore("images"); + const req = store.put(blob, key); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + tx.onabort = () => reject(tx.error || new Error("transaction aborted")); + tx.onerror = () => reject(tx.error || new Error("transaction error")); + } catch (err) { + reject(err); + } + }); +} + +async function getBlobFromIDB(key: string) { + const db = await openDB(); + return new Promise((resolve, reject) => { + try { + const tx = db.transaction("images", "readonly"); + const req = tx.objectStore("images").get(key); + req.onsuccess = () => resolve(req.result as Blob | undefined); + req.onerror = () => reject(req.error); + } catch (err) { + reject(err); + } + }); +} + +/* ---------- Video readiness helper ---------- */ + +async function waitForVideoReady(video: HTMLVideoElement, timeoutMs = 2500) { + const start = performance.now(); + + // Ensure play has been attempted + if (video.paused) { + try { + await video.play(); + } catch { + // ignore (autoplay restrictions, etc.) + } + } + + // Wait for dimensions + while ( + (!video.videoWidth || !video.videoHeight) && + performance.now() - start < timeoutMs + ) { + await new Promise((r) => setTimeout(r, 25)); + } + + // Wait for at least one painted frame + if ("requestVideoFrameCallback" in video) { + await new Promise((resolve) => { + // requestVideoFrameCallback is not yet in TypeScript's default DOM types + (video as any).requestVideoFrameCallback(() => resolve()); + }); + } else { + await new Promise((r) => requestAnimationFrame(() => r())); + await new Promise((r) => requestAnimationFrame(() => r())); + } + + // tiny extra delay helps some webcams/browsers + await new Promise((r) => setTimeout(r, 50)); +} + +/* ---------- Component ---------- */ + +export default function SelfieCapture({ + id = "user-selfie", + compact = false, +}: { + id?: string; + compact?: boolean; +}) { + const videoRef = useRef(null); + const canvasRef = useRef(null); + const streamRef = useRef(null); + const isMountedRef = useRef(true); + + const [isCameraOn, setCameraOn] = useState(false); + const [previewUrl, setPreviewUrl] = useState(null); + + useEffect(() => { + isMountedRef.current = true; + + (async () => { + try { + const blob = await getBlobFromIDB(id); + if (!isMountedRef.current) return; + if (blob) setPreviewUrl(URL.createObjectURL(blob)); + } catch (e) { + console.warn("Failed to load selfie from IDB", e); + } + })(); + + return () => { + isMountedRef.current = false; + stopCamera(); + setPreviewUrl((prev) => { + if (prev) URL.revokeObjectURL(prev); + return null; + }); + }; + + }, [id]); + + async function startCamera(autoCapture = false) { + try { + // IMPORTANT: ensures