From dd26de983344d5953369eccc2b987f92573426ec Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Sep 2026 15:46:04 -0700 Subject: [PATCH 1/8] feat: pre-hackathon workshop attendance QR management --- src/components/layout/app-sidebar.tsx | 6 + src/lib/firebase/types.ts | 8 + src/routeTree.gen.ts | 23 ++ .../hackathons/$hackathonId/attendance.tsx | 210 ++++++++++++++++++ src/services/prehackathon.ts | 66 ++++++ 5 files changed, 313 insertions(+) create mode 100644 src/routes/_auth/hackathons/$hackathonId/attendance.tsx create mode 100644 src/services/prehackathon.ts diff --git a/src/components/layout/app-sidebar.tsx b/src/components/layout/app-sidebar.tsx index 15919a4..d56f1ff 100644 --- a/src/components/layout/app-sidebar.tsx +++ b/src/components/layout/app-sidebar.tsx @@ -5,6 +5,7 @@ import { BadgeCheck, Bot, Calendar, + CalendarCheck, DollarSign, FileText, HelpCircle, @@ -112,6 +113,11 @@ export const generateHackathonNavigation = (hackathons: Hackathon[]) => { href: `/hackathons/${hackathon._id}/schedule`, icon: Calendar, }, + { + label: "Attendance", + href: `/hackathons/${hackathon._id}/attendance`, + icon: CalendarCheck, + }, { label: "Rewards", href: `/hackathons/${hackathon._id}/rewards`, diff --git a/src/lib/firebase/types.ts b/src/lib/firebase/types.ts index 8857c24..beb0687 100644 --- a/src/lib/firebase/types.ts +++ b/src/lib/firebase/types.ts @@ -100,6 +100,14 @@ export interface HackathonDayOf { lastModifiedBy?: string; } +export interface PreHackathonWorkshop { + _id?: string; + eventId?: string; + name?: string; + lastModified?: Timestamp; + lastModifiedBy?: string; +} + /** * Sub-collection: /Hackathon/[hackathon]/Projects * diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 3130806..90385a6 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -22,6 +22,7 @@ import { Route as AuthStampbookIndexRouteImport } from './routes/_auth/stampbook import { Route as AuthStampbookRaffleRouteImport } from './routes/_auth/stampbook/raffle' import { Route as AuthHackathonsHackathonIdIndexRouteImport } from './routes/_auth/hackathons/$hackathonId/index' import { Route as AuthHackathonsHackathonIdApplicationRouteImport } from './routes/_auth/hackathons/$hackathonId/application' +import { Route as AuthHackathonsHackathonIdAttendanceRouteImport } from './routes/_auth/hackathons/$hackathonId/attendance' import { Route as AuthHackathonsHackathonIdRewardsRouteImport } from './routes/_auth/hackathons/$hackathonId/rewards' import { Route as AuthHackathonsHackathonIdScheduleRouteImport } from './routes/_auth/hackathons/$hackathonId/schedule' import { Route as AuthHackathonsHackathonIdSettingsRouteImport } from './routes/_auth/hackathons/$hackathonId/settings' @@ -94,6 +95,12 @@ const AuthHackathonsHackathonIdApplicationRoute = path: '/application', getParentRoute: () => AuthHackathonsHackathonIdRouteRoute, } as any) +const AuthHackathonsHackathonIdAttendanceRoute = + AuthHackathonsHackathonIdAttendanceRouteImport.update({ + id: '/attendance', + path: '/attendance', + getParentRoute: () => AuthHackathonsHackathonIdRouteRoute, + } as any) const AuthHackathonsHackathonIdRewardsRoute = AuthHackathonsHackathonIdRewardsRouteImport.update({ id: '/rewards', @@ -131,6 +138,7 @@ export interface FileRoutesByFullPath { '/stampbook/raffle': typeof AuthStampbookRaffleRoute '/stampbook/': typeof AuthStampbookIndexRoute '/hackathons/$hackathonId/application': typeof AuthHackathonsHackathonIdApplicationRoute + '/hackathons/$hackathonId/attendance': typeof AuthHackathonsHackathonIdAttendanceRoute '/hackathons/$hackathonId/rewards': typeof AuthHackathonsHackathonIdRewardsRoute '/hackathons/$hackathonId/schedule': typeof AuthHackathonsHackathonIdScheduleRoute '/hackathons/$hackathonId/settings': typeof AuthHackathonsHackathonIdSettingsRoute @@ -148,6 +156,7 @@ export interface FileRoutesByTo { '/stampbook/raffle': typeof AuthStampbookRaffleRoute '/stampbook': typeof AuthStampbookIndexRoute '/hackathons/$hackathonId/application': typeof AuthHackathonsHackathonIdApplicationRoute + '/hackathons/$hackathonId/attendance': typeof AuthHackathonsHackathonIdAttendanceRoute '/hackathons/$hackathonId/rewards': typeof AuthHackathonsHackathonIdRewardsRoute '/hackathons/$hackathonId/schedule': typeof AuthHackathonsHackathonIdScheduleRoute '/hackathons/$hackathonId/settings': typeof AuthHackathonsHackathonIdSettingsRoute @@ -168,6 +177,7 @@ export interface FileRoutesById { '/_auth/stampbook/raffle': typeof AuthStampbookRaffleRoute '/_auth/stampbook/': typeof AuthStampbookIndexRoute '/_auth/hackathons/$hackathonId/application': typeof AuthHackathonsHackathonIdApplicationRoute + '/_auth/hackathons/$hackathonId/attendance': typeof AuthHackathonsHackathonIdAttendanceRoute '/_auth/hackathons/$hackathonId/rewards': typeof AuthHackathonsHackathonIdRewardsRoute '/_auth/hackathons/$hackathonId/schedule': typeof AuthHackathonsHackathonIdScheduleRoute '/_auth/hackathons/$hackathonId/settings': typeof AuthHackathonsHackathonIdSettingsRoute @@ -188,6 +198,7 @@ export interface FileRouteTypes { | '/stampbook/raffle' | '/stampbook/' | '/hackathons/$hackathonId/application' + | '/hackathons/$hackathonId/attendance' | '/hackathons/$hackathonId/rewards' | '/hackathons/$hackathonId/schedule' | '/hackathons/$hackathonId/settings' @@ -205,6 +216,7 @@ export interface FileRouteTypes { | '/stampbook/raffle' | '/stampbook' | '/hackathons/$hackathonId/application' + | '/hackathons/$hackathonId/attendance' | '/hackathons/$hackathonId/rewards' | '/hackathons/$hackathonId/schedule' | '/hackathons/$hackathonId/settings' @@ -224,6 +236,7 @@ export interface FileRouteTypes { | '/_auth/stampbook/raffle' | '/_auth/stampbook/' | '/_auth/hackathons/$hackathonId/application' + | '/_auth/hackathons/$hackathonId/attendance' | '/_auth/hackathons/$hackathonId/rewards' | '/_auth/hackathons/$hackathonId/schedule' | '/_auth/hackathons/$hackathonId/settings' @@ -329,6 +342,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthHackathonsHackathonIdApplicationRouteImport parentRoute: typeof AuthHackathonsHackathonIdRouteRoute } + '/_auth/hackathons/$hackathonId/attendance': { + id: '/_auth/hackathons/$hackathonId/attendance' + path: '/attendance' + fullPath: '/hackathons/$hackathonId/attendance' + preLoaderRoute: typeof AuthHackathonsHackathonIdAttendanceRouteImport + parentRoute: typeof AuthHackathonsHackathonIdRouteRoute + } '/_auth/hackathons/$hackathonId/rewards': { id: '/_auth/hackathons/$hackathonId/rewards' path: '/rewards' @@ -362,6 +382,7 @@ declare module '@tanstack/react-router' { interface AuthHackathonsHackathonIdRouteRouteChildren { AuthHackathonsHackathonIdApplicationRoute: typeof AuthHackathonsHackathonIdApplicationRoute + AuthHackathonsHackathonIdAttendanceRoute: typeof AuthHackathonsHackathonIdAttendanceRoute AuthHackathonsHackathonIdRewardsRoute: typeof AuthHackathonsHackathonIdRewardsRoute AuthHackathonsHackathonIdScheduleRoute: typeof AuthHackathonsHackathonIdScheduleRoute AuthHackathonsHackathonIdSettingsRoute: typeof AuthHackathonsHackathonIdSettingsRoute @@ -373,6 +394,8 @@ const AuthHackathonsHackathonIdRouteRouteChildren: AuthHackathonsHackathonIdRout { AuthHackathonsHackathonIdApplicationRoute: AuthHackathonsHackathonIdApplicationRoute, + AuthHackathonsHackathonIdAttendanceRoute: + AuthHackathonsHackathonIdAttendanceRoute, AuthHackathonsHackathonIdRewardsRoute: AuthHackathonsHackathonIdRewardsRoute, AuthHackathonsHackathonIdScheduleRoute: diff --git a/src/routes/_auth/hackathons/$hackathonId/attendance.tsx b/src/routes/_auth/hackathons/$hackathonId/attendance.tsx new file mode 100644 index 0000000..db5c644 --- /dev/null +++ b/src/routes/_auth/hackathons/$hackathonId/attendance.tsx @@ -0,0 +1,210 @@ +import { PageHeader } from "@/components/graphy/typo"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import type { PreHackathonWorkshop } from "@/lib/firebase/types"; +import { useHackathon } from "@/providers/hackathon-provider"; +import { + buildPreHackathonQrUrl, + deletePreHackathonWorkshop, + subscribeToPreHackathonWorkshops, + upsertPreHackathonWorkshop, +} from "@/services/prehackathon"; +import { createFileRoute } from "@tanstack/react-router"; +import { Copy, Download, Loader2, Plus, Trash2 } from "lucide-react"; +import QRCode from "qrcode"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; + +export const Route = createFileRoute("/_auth/hackathons/$hackathonId/attendance")({ + component: AttendanceComponent, +}); + +const toEventId = (name: string) => + name + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); + +function WorkshopQrCard({ + workshop, + qrUrl, + onDelete, +}: { + workshop: PreHackathonWorkshop; + qrUrl: string; + onDelete: () => void; +}) { + const [dataUrl, setDataUrl] = useState(null); + + useEffect(() => { + let active = true; + QRCode.toDataURL(qrUrl, { width: 200, margin: 2 }) + .then((url) => { + if (active) setDataUrl(url); + }) + .catch(console.error); + return () => { + active = false; + }; + }, [qrUrl]); + + const copyLink = async () => { + await navigator.clipboard.writeText(qrUrl); + toast.success("Attendance link copied"); + }; + + const download = () => { + if (!dataUrl) return; + const link = document.createElement("a"); + link.href = dataUrl; + link.download = `attendance-${workshop.eventId}.png`; + link.click(); + }; + + return ( + + +
+ {dataUrl ? ( + {`QR + ) : ( + + )} +
+
+

{workshop.name}

+

{workshop.eventId}

+
+ + + +
+

+ Scanning this QR opens the portal where hackers sign in and get marked attended. +

+
+
+
+ ); +} + +function AttendanceComponent() { + const { activeHackathon } = useHackathon(); + const [workshops, setWorkshops] = useState([]); + const [name, setName] = useState(""); + + useEffect(() => { + if (!activeHackathon) return; + + const unsubWorkshops = subscribeToPreHackathonWorkshops(activeHackathon, setWorkshops); + + return () => { + unsubWorkshops(); + }; + }, [activeHackathon]); + + const addWorkshop = async () => { + const eventId = toEventId(name); + if (!eventId) { + toast.error("Workshop name can't be empty"); + return; + } + if (workshops.some((workshop) => workshop.eventId === eventId)) { + toast.error(`A workshop with slug "${eventId}" already exists`); + return; + } + try { + await upsertPreHackathonWorkshop(activeHackathon, eventId, name.trim()); + setName(""); + toast.success("Workshop added"); + } catch { + toast.error("Failed to add workshop"); + } + }; + + const removeWorkshop = async (workshop: PreHackathonWorkshop) => { + if (!workshop.eventId) return; + if ( + !window.confirm( + `Delete "${workshop.name}"? Hackers already marked attended keep their record.`, + ) + ) + return; + try { + await deletePreHackathonWorkshop(activeHackathon, workshop.eventId); + toast.success("Workshop deleted"); + } catch { + toast.error("Failed to delete workshop"); + } + }; + + return ( +
+ + Attendance + {activeHackathon} + + + + +

Pre-hackathon workshops

+

+ Runs before the event. Hackers check in by scanning the QR on the portal. +

+
+ +
+ setName(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") addWorkshop(); + }} + placeholder="e.g. Intro to React" + /> + +
+ {workshops.length === 0 ? ( +

No pre-hackathon workshops yet.

+ ) : ( +
+ {workshops.map((workshop) => + workshop.eventId ? ( + removeWorkshop(workshop)} + /> + ) : null, + )} +
+ )} +
+
+
+ ); +} diff --git a/src/services/prehackathon.ts b/src/services/prehackathon.ts new file mode 100644 index 0000000..6bb7109 --- /dev/null +++ b/src/services/prehackathon.ts @@ -0,0 +1,66 @@ +import { auth, db } from "@/lib/firebase/client"; +import type { PreHackathonWorkshop } from "@/lib/firebase/types"; +import { getHackathonType } from "@/lib/utils"; +import { + Timestamp, + collection, + deleteDoc, + doc, + onSnapshot, + query, + setDoc, +} from "firebase/firestore"; + +const PORTAL_BASE_URL = import.meta.env.VITE_QR_PORTAL_ORIGIN || "https://portal.nwplus.io"; + +export const subscribeToPreHackathonWorkshops = ( + hackathon: string, + callback: (docs: PreHackathonWorkshop[]) => void, +) => + onSnapshot( + query(collection(db, "Hackathons", hackathon, "PreHackathonWorkshops")), + (querySnapshot) => { + const workshops = []; + for (const workshop of querySnapshot.docs) { + workshops.push({ + ...(workshop.data() as unknown as PreHackathonWorkshop), + _id: workshop.id, + }); + } + callback(workshops); + }, + ); + +export const upsertPreHackathonWorkshop = async ( + hackathon: string, + eventId: string, + name: string, +) => { + try { + const record = { + lastModified: Timestamp.now(), + lastModifiedBy: auth.currentUser?.email ?? "", + }; + + await setDoc( + doc(db, "Hackathons", hackathon, "PreHackathonWorkshops", eventId), + { eventId, name, ...record }, + { merge: true }, + ); + } catch (error) { + console.error(error); + throw error; + } +}; + +export const deletePreHackathonWorkshop = async (hackathon: string, eventId: string) => { + if (!eventId) return; + try { + await deleteDoc(doc(db, "Hackathons", hackathon, "PreHackathonWorkshops", eventId)); + } catch (error) { + console.log(error); + } +}; + +export const buildPreHackathonQrUrl = (hackathonId: string, eventId: string, name: string) => + `${PORTAL_BASE_URL}/${getHackathonType(hackathonId)}/attendance?event=${eventId}&name=${encodeURIComponent(name)}`; From 7a0e1aae0bbd6de316647d54d518ee9c80f08cef Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 24 Sep 2026 21:57:22 -0700 Subject: [PATCH 2/8] fix: rethrow delete error so failure toasts correctly --- src/services/prehackathon.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/prehackathon.ts b/src/services/prehackathon.ts index 6bb7109..857cba7 100644 --- a/src/services/prehackathon.ts +++ b/src/services/prehackathon.ts @@ -58,7 +58,8 @@ export const deletePreHackathonWorkshop = async (hackathon: string, eventId: str try { await deleteDoc(doc(db, "Hackathons", hackathon, "PreHackathonWorkshops", eventId)); } catch (error) { - console.log(error); + console.error(error); + throw error; } }; From 09f445384a53973fb23b55c8d6086e82d6a8f2cd Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 25 Sep 2026 13:48:22 -0700 Subject: [PATCH 3/8] feat: add individual github, linkedin, portfolio website, and resume question types --- src/components/features/hackerapp/hacker-app-question.tsx | 8 ++++++++ src/lib/firebase/types.ts | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/features/hackerapp/hacker-app-question.tsx b/src/components/features/hackerapp/hacker-app-question.tsx index 62c5295..b0f0c2b 100644 --- a/src/components/features/hackerapp/hacker-app-question.tsx +++ b/src/components/features/hackerapp/hacker-app-question.tsx @@ -44,6 +44,10 @@ const QUESTION_TYPES: HackerApplicationQuestionType[] = [ "School", "Major", "Country", + "Github", + "LinkedIn", + "Portfolio Website", + "Resume", ]; // Question types that if selected, bricks the select field and locks in the value @@ -53,6 +57,10 @@ const QUESTION_TYPES_UNIQUE: HackerApplicationQuestionType[] = [ "School", "Major", "Country", + "Github", + "LinkedIn", + "Portfolio Website", + "Resume", ]; export const SHOW_FORM_INPUT: HackerApplicationQuestionType[] = [ diff --git a/src/lib/firebase/types.ts b/src/lib/firebase/types.ts index a3a81ae..afdca89 100644 --- a/src/lib/firebase/types.ts +++ b/src/lib/firebase/types.ts @@ -350,7 +350,11 @@ export type HackerApplicationQuestionType = | "Dropdown" | "School" | "Major" - | "Country"; + | "Country" + | "Github" + | "LinkedIn" + | "Portfolio Website" + | "Resume"; // keep in sync with portal-v2 src/lib/firebase/types/hacker-app-questions.ts, which renders these export const FORM_INPUT_FIELDS = [ "academicYear", From afc46f49b1a5057c9a530b1a12f77c0da0d9d22b Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 25 Sep 2026 17:03:59 -0700 Subject: [PATCH 4/8] feat(hackerapp): pin fixed question types to their section --- .../hackerapp/hacker-app-question.tsx | 7 +++++- .../features/hackerapp/hacker-app-sections.ts | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/components/features/hackerapp/hacker-app-question.tsx b/src/components/features/hackerapp/hacker-app-question.tsx index b0f0c2b..fbe1dd5 100644 --- a/src/components/features/hackerapp/hacker-app-question.tsx +++ b/src/components/features/hackerapp/hacker-app-question.tsx @@ -31,7 +31,11 @@ import { getEligibleSources, } from "./hacker-app-conditions"; import type { UsedFieldsRegistry } from "./hacker-app-main"; -import { LEGAL_NAME_FORM_INPUTS, isFormInputAllowedInSection } from "./hacker-app-sections"; +import { + LEGAL_NAME_FORM_INPUTS, + isFormInputAllowedInSection, + isQuestionTypeAllowedInSection, +} from "./hacker-app-sections"; const QUESTION_TYPES: HackerApplicationQuestionType[] = [ "Long Answer", @@ -140,6 +144,7 @@ export const HackerAppQuestion = memo(function HackerAppQuestion({ ); const usableQuestionTypes = QUESTION_TYPES?.filter( (qt) => + isQuestionTypeAllowedInSection(qt, section) && (qt !== "Full Legal Name" || !hasSplitLegalName) && (!QUESTION_TYPES_UNIQUE.includes(qt) || !usedFieldsRegistry.questionType.has(qt)), ); diff --git a/src/components/features/hackerapp/hacker-app-sections.ts b/src/components/features/hackerapp/hacker-app-sections.ts index c1e9ab4..575c4eb 100644 --- a/src/components/features/hackerapp/hacker-app-sections.ts +++ b/src/components/features/hackerapp/hacker-app-sections.ts @@ -1,5 +1,6 @@ import type { HackerApplicationQuestionFormInputField, + HackerApplicationQuestionType, HackerApplicationSections, } from "@/lib/firebase/types"; @@ -69,3 +70,25 @@ export const isFormInputAllowedInSection = ( const pinned = SECTION_BY_FORM_INPUT[formInput]; return pinned === undefined || pinned === section; }; + +export const QUESTION_TYPE_SECTION: Partial< + Record +> = { + "Full Legal Name": "BasicInfo", + School: "BasicInfo", + Major: "BasicInfo", + Country: "BasicInfo", + Portfolio: "Skills", + Github: "Skills", + LinkedIn: "Skills", + "Portfolio Website": "Skills", + Resume: "Skills", +}; + +export const isQuestionTypeAllowedInSection = ( + questionType: HackerApplicationQuestionType, + section: HackerApplicationSections, +): boolean => { + const pinned = QUESTION_TYPE_SECTION[questionType]; + return pinned === undefined || pinned === section; +}; From aee8af2b77d0c97c213f3d646b9549381497d909 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 25 Sep 2026 17:07:30 -0700 Subject: [PATCH 5/8] fix(hackerapp): limit section gating to skills types --- src/components/features/hackerapp/hacker-app-sections.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/features/hackerapp/hacker-app-sections.ts b/src/components/features/hackerapp/hacker-app-sections.ts index 575c4eb..dfa5f9b 100644 --- a/src/components/features/hackerapp/hacker-app-sections.ts +++ b/src/components/features/hackerapp/hacker-app-sections.ts @@ -74,10 +74,6 @@ export const isFormInputAllowedInSection = ( export const QUESTION_TYPE_SECTION: Partial< Record > = { - "Full Legal Name": "BasicInfo", - School: "BasicInfo", - Major: "BasicInfo", - Country: "BasicInfo", Portfolio: "Skills", Github: "Skills", LinkedIn: "Skills", From 86b6f9fc2f42348265975a46a3673ed3c5ff2424 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 25 Sep 2026 17:18:02 -0700 Subject: [PATCH 6/8] fix(evaluator): don't linkify non-url answers --- .../features/evaluator/response.tsx | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/components/features/evaluator/response.tsx b/src/components/features/evaluator/response.tsx index 110d2d0..0433c35 100644 --- a/src/components/features/evaluator/response.tsx +++ b/src/components/features/evaluator/response.tsx @@ -65,30 +65,29 @@ const BooleanMap = ({ value }: { value?: Record }) => { }; const LinkField = ({ href }: { href?: string }) => { + const value = href ?? ""; + const isUrl = /^https?:\/\//i.test(value.trim()); + const handleClipboard = () => { - if (!href) return; - navigator.clipboard.writeText(href); + if (!value) return; + navigator.clipboard.writeText(value); toast("Copied link to clipboard!"); }; - const hasLink = Boolean(href); + if (!isUrl) { + return ; + } return ( From 43de2162bc841ee0060c3a10e39db035aa6bfa30 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 25 Sep 2026 17:31:24 -0700 Subject: [PATCH 7/8] fix(evaluator): show uploaded state for resumes --- .../features/evaluator/response.tsx | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/components/features/evaluator/response.tsx b/src/components/features/evaluator/response.tsx index 0433c35..5707bdb 100644 --- a/src/components/features/evaluator/response.tsx +++ b/src/components/features/evaluator/response.tsx @@ -44,10 +44,12 @@ export function Response({ label, type, response, userId }: ResponseProps) { ) : type === "booleanMap" ? ( } /> + ) : type === "resume" ? ( + ) : loading ? ( ) : ( - + )} ); @@ -64,6 +66,38 @@ const BooleanMap = ({ value }: { value?: Record }) => { ); }; +const ResumeField = ({ href, loading }: { href?: string; loading: boolean }) => { + if (loading) return ; + + const value = href ?? ""; + if (!value.trim()) { + return ; + } + + const handleClipboard = () => { + navigator.clipboard.writeText(value); + toast("Copied link to clipboard!"); + }; + + return ( +
+ Uploaded + + View resume + + + +
+ ); +}; + const LinkField = ({ href }: { href?: string }) => { const value = href ?? ""; const isUrl = /^https?:\/\//i.test(value.trim()); From b40609896980255b903fdc43af1bfbca83ed33cb Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 25 Sep 2026 17:42:39 -0700 Subject: [PATCH 8/8] style(evaluator): format link field --- src/components/features/evaluator/response.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/features/evaluator/response.tsx b/src/components/features/evaluator/response.tsx index 5707bdb..b1ca670 100644 --- a/src/components/features/evaluator/response.tsx +++ b/src/components/features/evaluator/response.tsx @@ -114,10 +114,12 @@ const LinkField = ({ href }: { href?: string }) => { return (