diff --git a/app/bounty/[bountyId]/page.tsx b/app/bounty/[bountyId]/page.tsx index 304edb4..608d020 100644 --- a/app/bounty/[bountyId]/page.tsx +++ b/app/bounty/[bountyId]/page.tsx @@ -14,7 +14,10 @@ export default async function BountyDetailPage({ params }: Props) { {/* Ambient glow – matches BountiesPage */}
-
+
{/* Breadcrumb */}
diff --git a/components/bounty/competition-judging.tsx b/components/bounty/competition-judging.tsx new file mode 100644 index 0000000..78e5388 --- /dev/null +++ b/components/bounty/competition-judging.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { toast } from "sonner"; +import { authClient } from "@/lib/auth-client"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { useCompetitionBounty } from "@/hooks/use-competition-bounty"; + +type SessionUser = { + id: string; + name?: string | null; +}; + +export function CompetitionJudging({ + bountyId, + creatorId, + deadline, + rewardAmount, + maxParticipants = 5, +}: { + bountyId: string; + creatorId: string; + deadline?: string | null; + rewardAmount: number; + maxParticipants?: number; +}) { + const { data: session } = authClient.useSession(); + const user = session?.user as SessionUser | undefined; + const [payoutBySubmission, setPayoutBySubmission] = useState< + Record + >({}); + const [pointsBySubmission, setPointsBySubmission] = useState< + Record + >({}); + + const { + state, + isAfterDeadline, + revealedSubmissions, + approveContestWinner, + finalizeContest, + } = useCompetitionBounty({ + bountyId, + submissionDeadline: deadline, + maxParticipants, + }); + + const isCreator = user?.id === creatorId; + + const awardedUserIds = useMemo( + () => new Set(state.awards.map((it) => it.recipientUserId)), + [state.awards], + ); + + const handleAward = (submissionId: string, isWinner: boolean) => { + const submission = revealedSubmissions.find((it) => it.id === submissionId); + if (!submission) return; + + const payout = Number(payoutBySubmission[submissionId] || "0"); + const points = Number(pointsBySubmission[submissionId] || "0"); + if (!Number.isFinite(payout) || payout <= 0) { + toast.error("Enter a valid payout amount."); + return; + } + + approveContestWinner({ + recipientUserId: submission.participantUserId, + recipientDisplayName: submission.participantDisplayName, + payoutAmount: payout, + points: Number.isFinite(points) ? points : 0, + isWinner, + }); + toast.success(isWinner ? "Winner selected." : "Consolation awarded."); + }; + + const handleFinalize = () => { + if (state.awards.length === 0) { + toast.error("Select at least one winner or consolation award first."); + return; + } + finalizeContest(); + toast.success("Contest finalized."); + }; + + if (!isCreator) return null; + + return ( +
+
+

+ Competition Judging +

+ + Prize pool: ${rewardAmount} + +
+ + {!isAfterDeadline ? ( +

+ Judging opens after the submission deadline. +

+ ) : revealedSubmissions.length === 0 ? ( +

No submissions to review.

+ ) : ( +
+ {revealedSubmissions.map((submission, index) => { + const alreadyAwarded = awardedUserIds.has( + submission.participantUserId, + ); + return ( +
+
+

+ Submission #{index + 1} +

+ + {new Date(submission.submittedAt).toLocaleString()} + +
+

+ {submission.workCid} +

+ +
+ + setPayoutBySubmission((prev) => ({ + ...prev, + [submission.id]: event.target.value, + })) + } + disabled={state.finalized || alreadyAwarded} + /> + + setPointsBySubmission((prev) => ({ + ...prev, + [submission.id]: event.target.value, + })) + } + disabled={state.finalized || alreadyAwarded} + /> +
+ +
+ + +
+
+ ); + })} +
+ )} + +
+

+ {state.finalized + ? "Contest finalized" + : "Finalizing locks further awards."} +

+ +
+
+ ); +} diff --git a/components/bounty/competition-status.tsx b/components/bounty/competition-status.tsx new file mode 100644 index 0000000..31f7bcc --- /dev/null +++ b/components/bounty/competition-status.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useCompetitionBounty } from "@/hooks/use-competition-bounty"; + +export function CompetitionStatus({ + bountyId, + deadline, + maxParticipants = 5, +}: { + bountyId: string; + deadline?: string | null; + maxParticipants?: number; +}) { + const { state, isAfterDeadline, winnerAwards, consolationAwards } = + useCompetitionBounty({ + bountyId, + submissionDeadline: deadline, + maxParticipants, + }); + + const totalSubmissions = state.submissions.length; + + return ( +
+

+ Competition Status +

+ + {!isAfterDeadline ? ( +

+ {totalSubmissions} submissions received (hidden until deadline) +

+ ) : ( + <> +

+ Reveal complete: {totalSubmissions} submissions visible +

+ {state.submissions.length > 0 && ( +
+ {state.submissions.map((submission) => ( +
+

+ {submission.workCid} +

+
+ ))} +
+ )} + + )} + + {winnerAwards.length > 0 && ( +
+ Winner{winnerAwards.length > 1 ? "s" : ""}:{" "} + {winnerAwards + .map( + (award) => + `${award.recipientDisplayName} ($${award.payoutAmount})`, + ) + .join(", ")} +
+ )} + + {consolationAwards.length > 0 && ( +
+ Consolation:{" "} + {consolationAwards + .map( + (award) => + `${award.recipientDisplayName} ($${award.payoutAmount})`, + ) + .join(", ")} +
+ )} + + {state.finalized && ( +

+ Contest finalized. Further winner approvals are disabled. +

+ )} +
+ ); +} diff --git a/components/bounty/competition-submission.tsx b/components/bounty/competition-submission.tsx new file mode 100644 index 0000000..8511204 --- /dev/null +++ b/components/bounty/competition-submission.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { formatDistanceToNowStrict } from "date-fns"; +import { Clock, Loader2 } from "lucide-react"; +import { toast } from "sonner"; +import { authClient } from "@/lib/auth-client"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { useCompetitionBounty } from "@/hooks/use-competition-bounty"; + +type SessionUser = { + id: string; + name?: string | null; +}; + +export function CompetitionSubmission({ + bountyId, + deadline, + maxParticipants = 5, +}: { + bountyId: string; + deadline?: string | null; + maxParticipants?: number; +}) { + const { data: session } = authClient.useSession(); + const user = session?.user as SessionUser | undefined; + const [workCid, setWorkCid] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + const { + state, + participantCount, + isAfterDeadline, + joinCompetition, + submitWork, + } = useCompetitionBounty({ + bountyId, + submissionDeadline: deadline, + maxParticipants, + }); + + const displayName = user?.name?.trim() || "Contributor"; + const isParticipant = Boolean( + user?.id && state.participants.some((it) => it.userId === user.id), + ); + const hasSubmitted = Boolean( + user?.id && + state.submissions.some((it) => it.participantUserId === user.id), + ); + const isFull = participantCount >= maxParticipants; + + const deadlineLabel = useMemo(() => { + if (!deadline) return "No deadline configured"; + const asDate = new Date(deadline); + if (Number.isNaN(asDate.getTime())) return "No deadline configured"; + if (isAfterDeadline) return "Submission window closed"; + return `${formatDistanceToNowStrict(asDate)} remaining`; + }, [deadline, isAfterDeadline]); + + const handleJoin = () => { + if (!user?.id) { + toast.error("Sign in to join this competition."); + return; + } + if (isFull && !isParticipant) { + toast.error("Competition is full."); + return; + } + joinCompetition({ userId: user.id, displayName }); + toast.success("Joined competition."); + }; + + const handleSubmit = async () => { + if (!user?.id) { + toast.error("Sign in to submit work."); + return; + } + if (!isParticipant) { + toast.error("Join the competition before submitting."); + return; + } + if (!workCid.trim()) return; + setIsSubmitting(true); + try { + submitWork({ + participantUserId: user.id, + participantDisplayName: displayName, + workCid: workCid.trim(), + }); + toast.success( + hasSubmitted ? "Submission updated." : "Submission received.", + ); + setWorkCid(""); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+
+

+ Competition Entry +

+

+ {participantCount}/{maxParticipants} joined +

+
+
+
+ + {deadlineLabel} +
+
+
+ +
+ {state.participants.length > 0 ? ( + state.participants.map((participant) => ( + + {participant.displayName} + + )) + ) : ( +

No participants yet.

+ )} +
+ + + + {isParticipant && !isAfterDeadline && ( +
+ setWorkCid(event.target.value)} + /> + +

+ Submissions remain hidden from all participants until deadline. +

+
+ )} + + {isAfterDeadline && ( +

+ Submission window closed. All entries are now revealed for judging. +

+ )} +
+ ); +} diff --git a/hooks/use-competition-bounty.ts b/hooks/use-competition-bounty.ts new file mode 100644 index 0000000..9ea91be --- /dev/null +++ b/hooks/use-competition-bounty.ts @@ -0,0 +1,216 @@ +"use client"; + +import { useMemo } from "react"; +import { useLocalStorage } from "@/hooks/use-local-storage"; + +export type CompetitionParticipant = { + userId: string; + displayName: string; + joinedAt: string; +}; + +export type CompetitionSubmission = { + id: string; + participantUserId: string; + participantDisplayName: string; + workCid: string; + submittedAt: string; +}; + +export type CompetitionAward = { + id: string; + recipientUserId: string; + recipientDisplayName: string; + payoutAmount: number; + points: number; + isWinner: boolean; + createdAt: string; +}; + +type CompetitionState = { + participants: CompetitionParticipant[]; + submissions: CompetitionSubmission[]; + awards: CompetitionAward[]; + finalized: boolean; + finalizedAt?: string; +}; + +const DEFAULT_MAX_PARTICIPANTS = 5; + +function makeId(prefix: string) { + return `${prefix}-${Math.random().toString(36).slice(2, 10)}`; +} + +function hashString(value: string) { + let hash = 0; + for (let i = 0; i < value.length; i += 1) { + hash = (hash << 5) - hash + value.charCodeAt(i); + hash |= 0; + } + return hash; +} + +export function useCompetitionBounty({ + bountyId, + submissionDeadline, + maxParticipants = DEFAULT_MAX_PARTICIPANTS, +}: { + bountyId: string; + submissionDeadline?: string | null; + maxParticipants?: number; +}) { + const [state, setState] = useLocalStorage( + `competition-bounty:${bountyId}`, + { + participants: [], + submissions: [], + awards: [], + finalized: false, + }, + ); + + const deadlineDate = submissionDeadline ? new Date(submissionDeadline) : null; + const now = new Date(); + const isAfterDeadline = Boolean( + deadlineDate && + !Number.isNaN(deadlineDate.getTime()) && + now >= deadlineDate, + ); + + const remainingSlots = Math.max( + 0, + maxParticipants - state.participants.length, + ); + const participantCount = state.participants.length; + + const joinCompetition = (participant: { + userId: string; + displayName: string; + }) => { + setState((prev) => { + if (prev.finalized) return prev; + const alreadyJoined = prev.participants.some( + (it) => it.userId === participant.userId, + ); + if (alreadyJoined || prev.participants.length >= maxParticipants) { + return prev; + } + return { + ...prev, + participants: [ + ...prev.participants, + { + userId: participant.userId, + displayName: participant.displayName, + joinedAt: new Date().toISOString(), + }, + ], + }; + }); + }; + + const submitWork = (input: { + participantUserId: string; + participantDisplayName: string; + workCid: string; + }) => { + setState((prev) => { + if (prev.finalized) return prev; + if (isAfterDeadline) return prev; + const participantExists = prev.participants.some( + (it) => it.userId === input.participantUserId, + ); + if (!participantExists) return prev; + const hasSubmission = prev.submissions.some( + (it) => it.participantUserId === input.participantUserId, + ); + const nextSubmission: CompetitionSubmission = { + id: hasSubmission + ? (prev.submissions.find( + (it) => it.participantUserId === input.participantUserId, + )?.id ?? makeId("sub")) + : makeId("sub"), + participantUserId: input.participantUserId, + participantDisplayName: input.participantDisplayName, + workCid: input.workCid, + submittedAt: new Date().toISOString(), + }; + + return { + ...prev, + submissions: hasSubmission + ? prev.submissions.map((it) => + it.participantUserId === input.participantUserId + ? nextSubmission + : it, + ) + : [...prev.submissions, nextSubmission], + }; + }); + }; + + const approveContestWinner = (input: { + recipientUserId: string; + recipientDisplayName: string; + payoutAmount: number; + points: number; + isWinner: boolean; + }) => { + setState((prev) => { + if (prev.finalized || !isAfterDeadline) return prev; + return { + ...prev, + awards: [ + ...prev.awards, + { + id: makeId("award"), + recipientUserId: input.recipientUserId, + recipientDisplayName: input.recipientDisplayName, + payoutAmount: input.payoutAmount, + points: input.points, + isWinner: input.isWinner, + createdAt: new Date().toISOString(), + }, + ], + }; + }); + }; + + const finalizeContest = () => { + setState((prev) => { + if (prev.finalized) return prev; + return { + ...prev, + finalized: true, + finalizedAt: new Date().toISOString(), + }; + }); + }; + + const revealedSubmissions = useMemo(() => { + if (!isAfterDeadline) return []; + return [...state.submissions].sort( + (a, b) => + hashString(`${bountyId}:${a.id}`) - hashString(`${bountyId}:${b.id}`), + ); + }, [bountyId, isAfterDeadline, state.submissions]); + + const winnerAwards = state.awards.filter((it) => it.isWinner); + const consolationAwards = state.awards.filter((it) => !it.isWinner); + + return { + state, + participantCount, + remainingSlots, + maxParticipants, + isAfterDeadline, + hasDeadline: Boolean(deadlineDate), + joinCompetition, + submitWork, + approveContestWinner, + finalizeContest, + revealedSubmissions, + winnerAwards, + consolationAwards, + }; +} diff --git a/hooks/use-local-storage.ts b/hooks/use-local-storage.ts index 46ff94a..4e19470 100644 --- a/hooks/use-local-storage.ts +++ b/hooks/use-local-storage.ts @@ -1,60 +1,95 @@ -import * as React from "react" +import * as React from "react"; /** * Hook to persist state in localStorage. - * + * * @param key The key to store the value under in localStorage. * @param initialValue The initial value to use if no value is found in localStorage. * @returns A tuple containing the stored value and a setter function. - * + * * @example * const [name, setName] = useLocalStorage("name", "John Doe"); */ export function useLocalStorage( - key: string, - initialValue: T + key: string, + initialValue: T, ): [T, (value: T | ((val: T) => T)) => void] { - // State to store our value - // Pass initial logic to useState to ensure it only runs once - const [storedValue, setStoredValue] = React.useState(() => { - if (typeof window === "undefined") { - return initialValue - } + // State to store our value + // Pass initial logic to useState to ensure it only runs once + const [storedValue, setStoredValue] = React.useState(() => { + if (typeof window === "undefined") { + return initialValue; + } + + try { + const item = window.localStorage.getItem(key); + // Parse stored json or if none return initialValue + return item ? JSON.parse(item) : initialValue; + } catch (error) { + // If error also return initialValue + console.warn(`Error reading localStorage key "${key}":`, error); + return initialValue; + } + }); + + React.useEffect(() => { + if (typeof window === "undefined") return; + + const readValue = () => { + try { + const item = window.localStorage.getItem(key); + setStoredValue(item ? JSON.parse(item) : initialValue); + } catch (error) { + console.warn(`Error reading localStorage key "${key}":`, error); + setStoredValue(initialValue); + } + }; + + const handleStorage = (event: StorageEvent) => { + if (event.key !== key) return; + readValue(); + }; + const handleLocalStorageUpdate = (event: Event) => { + const customEvent = event as CustomEvent<{ key: string }>; + if (customEvent.detail?.key !== key) return; + readValue(); + }; + + window.addEventListener("storage", handleStorage); + window.addEventListener("local-storage", handleLocalStorageUpdate); + return () => { + window.removeEventListener("storage", handleStorage); + window.removeEventListener("local-storage", handleLocalStorageUpdate); + }; + }, [initialValue, key]); + + // Return a wrapped version of useState's setter function that ... + // ... persists the new value to localStorage. + const setValue = React.useCallback( + (value: T | ((val: T) => T)) => { + // Use functional update to get the latest value + setStoredValue((currentValue) => { + const valueToStore = + value instanceof Function ? value(currentValue) : value; + + // Save to local storage (catch errors here so they don't escape React internals) try { - const item = window.localStorage.getItem(key) - // Parse stored json or if none return initialValue - return item ? JSON.parse(item) : initialValue + if (typeof window !== "undefined") { + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + window.dispatchEvent( + new CustomEvent("local-storage", { detail: { key } }), + ); + } } catch (error) { - // If error also return initialValue - console.warn(`Error reading localStorage key "${key}":`, error) - return initialValue + console.warn(`Error setting localStorage key "${key}":`, error); } - }) - - // Return a wrapped version of useState's setter function that ... - // ... persists the new value to localStorage. - const setValue = React.useCallback( - (value: T | ((val: T) => T)) => { - // Use functional update to get the latest value - setStoredValue((currentValue) => { - const valueToStore = - value instanceof Function ? value(currentValue) : value - - // Save to local storage (catch errors here so they don't escape React internals) - try { - if (typeof window !== "undefined") { - window.localStorage.setItem(key, JSON.stringify(valueToStore)) - } - } catch (error) { - console.warn(`Error setting localStorage key "${key}":`, error) - } - - return valueToStore - }) - }, - [key] - ) - - return [storedValue, setValue] + + return valueToStore; + }); + }, + [key], + ); + + return [storedValue, setValue]; } diff --git a/package-lock.json b/package-lock.json index 9adb979..2014dce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -188,6 +188,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1169,6 +1170,7 @@ "version": "1.4.18", "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.4.18.tgz", "integrity": "sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg==", + "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" @@ -1198,12 +1200,14 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.0.tgz", "integrity": "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@better-fetch/fetch": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz", - "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==" + "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==", + "peer": true }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", @@ -1317,6 +1321,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1340,6 +1345,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -8037,6 +8043,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.90.20" }, @@ -8071,6 +8078,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -8457,6 +8465,7 @@ "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -8466,6 +8475,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -8476,6 +8486,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -8572,6 +8583,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -9258,6 +9270,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -9956,6 +9969,7 @@ "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.1.8.tgz", "integrity": "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw==", "license": "MIT", + "peer": true, "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", @@ -10015,6 +10029,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -11277,7 +11292,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -11632,6 +11648,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -11833,6 +11850,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -12851,6 +12869,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -15371,6 +15390,7 @@ "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -15400,6 +15420,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -15533,6 +15554,7 @@ "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.11.tgz", "integrity": "sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==", "license": "MIT", + "peer": true, "engines": { "node": ">=20.0.0" } @@ -17319,6 +17341,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": "^20.0.0 || >=22.0.0" } @@ -17351,6 +17374,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", @@ -18573,6 +18597,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -18603,6 +18628,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -18615,6 +18641,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -20169,7 +20196,8 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.0.tgz", "integrity": "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -20296,6 +20324,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -20435,6 +20464,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -20645,6 +20675,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -21106,6 +21137,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -21199,6 +21231,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -21212,6 +21245,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -21681,6 +21715,7 @@ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -21862,6 +21897,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }