Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@typescript-eslint/parser": "^8.70.1",
"@typescript-eslint/utils": "^8.70.1",
"eslint": "^10.11.0",
"eslint-config-prettier": "^10.1.5",
"eslint-config-prettier": "^10.1.8",
"typescript": "^7.0.2"
}
}
319 changes: 263 additions & 56 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

12 changes: 7 additions & 5 deletions sites/hacklytics2027/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useSyncExternalStore } from "react";
import HomeSections from "@/components/HomeSections";
import PixelGarden, { PixelGround } from "@/components/pixel/PixelGarden";
import PixelSprite from "@/components/pixel/PixelSprite";
Expand Down Expand Up @@ -57,6 +57,8 @@ const FloralBackground = () => (
);

// ─── Countdown ────────────────────────────────────────────────────────────
const subscribeNoop = () => () => {};

const Countdown: React.FC<{ targetDate: Date }> = ({ targetDate }) => {
const getTimeLeft = React.useCallback(() => {
const distance = targetDate.getTime() - Date.now();
Expand All @@ -69,12 +71,12 @@ const Countdown: React.FC<{ targetDate: Date }> = ({ targetDate }) => {
};
}, [targetDate]);

const [timeLeft, setTimeLeft] = useState<{ days: number; hours: number; minutes: number; seconds: number } | null>(null);
const [mounted, setMounted] = useState(false);
const [timeLeft, setTimeLeft] = useState(getTimeLeft);
// False for the static HTML and hydration, true after: the build-time
// countdown would be stale by the time anyone loads the page.
const mounted = useSyncExternalStore(subscribeNoop, () => true, () => false);
Comment thread
cursor[bot] marked this conversation as resolved.

useEffect(() => {
setMounted(true);
setTimeLeft(getTimeLeft());
const id = setInterval(() => setTimeLeft(getTimeLeft()), 1000);
return () => clearInterval(id);
}, [getTimeLeft]);
Expand Down
6 changes: 0 additions & 6 deletions sites/hacklytics2027/components/LazySection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,6 @@ export default function LazySection({
const el = ref.current;
if (!el) return;

// If IntersectionObserver isn't available, render immediately
if (typeof IntersectionObserver === "undefined") {
setIsVisible(true);
return;
}

const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
Expand Down
42 changes: 27 additions & 15 deletions sites/mainweb/app/(portal)/admin/judging/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Zap } from "lucide-react";
import { useSession } from "next-auth/react";
import { trpc } from "@/lib/trpc";
import { usePortalContext } from "@/lib/use-portal-context";
import { useIsClient } from "@/lib/use-is-client";
import { useRouter } from "next/navigation";
import { LiquidGlass } from "@/components/portal/LiquidGlass";
import { JudgingTools } from "@/components/admin/judging/JudgingTools";
Expand All @@ -18,7 +19,7 @@ import { judgingPrepIsCurrent } from "@/lib/judging-prep";
export default function AdminResultsPage() {
const { data: session, status } = useSession();
const router = useRouter();
const [mounted, setMounted] = useState(false);
const mounted = useIsClient();
const [selectedHackathon, setSelectedHackathon] = useState<string | null>(
null,
);
Expand Down Expand Up @@ -76,7 +77,6 @@ export default function AdminResultsPage() {
null,
);
const selectedHackathonRef = useRef(selectedHackathon);
selectedHackathonRef.current = selectedHackathon;
const prepGen = useRef(0);

const promoteSubmissions = trpc.judge.promoteSubmissions.useMutation();
Expand Down Expand Up @@ -172,29 +172,41 @@ export default function AdminResultsPage() {
}
};

useEffect(() => {
setMounted(true);
}, []);

useEffect(() => {
if (status === "unauthenticated") {
router.push("/login");
}
}, [status, router]);

// Auto-select first hackathon
useEffect(() => {
if (hackathons?.[0] && !selectedHackathon) {
setSelectedHackathon(hackathons[0].id);
}
}, [hackathons, selectedHackathon]);

useEffect(() => {
prepGen.current += 1;
if (hackathons?.[0] && !selectedHackathon) {
setSelectedHackathon(hackathons[0].id);
}

// A new selection drops the previous edition's prep result and conflict.
const [prepShownFor, setPrepShownFor] = useState(selectedHackathon);
if (prepShownFor !== selectedHackathon) {
setPrepShownFor(selectedHackathon);
setAssignConflictId(null);
setPrepState({ busy: false, message: null, error: null });
}

// Covers the auto-select above, which runs before any prep can start.
useEffect(() => {
selectedHackathonRef.current = selectedHackathon;
}, [selectedHackathon]);
Comment thread
greptile-apps[bot] marked this conversation as resolved.

// The ref and generation move here, in the click, before B renders: a run
// still in flight for A fails stillThisRun from this moment, so its result
// cannot land on B's panel. Re-selecting the current edition is a no-op, so
// it cannot orphan that edition's own run with busy stuck on.
const selectHackathon = (id: string) => {
if (id === selectedHackathonRef.current) return;
selectedHackathonRef.current = id;
prepGen.current += 1;
setSelectedHackathon(id);
};

const categories = useMemo(() => {
if (!rankings?.rankings) return ["ALL"];
const cats = new Set(
Expand Down Expand Up @@ -451,7 +463,7 @@ export default function AdminResultsPage() {
<JudgingTools
hackathons={hackathons || []}
selectedHackathon={selectedHackathon}
setSelectedHackathon={setSelectedHackathon}
setSelectedHackathon={selectHackathon}
viewMode={viewMode}
setViewMode={setViewMode}
categories={categories}
Expand Down
6 changes: 4 additions & 2 deletions sites/mainweb/app/(portal)/admin/resumes/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@ export default function AdminResumesPage() {

// A selection or page carried across a filter change would put people in a
// book the list said it would not.
useEffect(() => {
const [filterShown, setFilterShown] = useState({ scope, debounced });
if (filterShown.scope !== scope || filterShown.debounced !== debounced) {
setFilterShown({ scope, debounced });
setSelected(new Set());
setPreview(null);
setOffset(0);
}, [scope, debounced]);
}

const allChecked = rows.length > 0 && rows.every((r) => selected.has(r.userId));

Expand Down
12 changes: 7 additions & 5 deletions sites/mainweb/app/(portal)/club/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,13 @@ export default function ClubPage() {
}
}, [status, memberStatus, router]);

useEffect(() => {
if (!showScanner) {
setScannedCodes(new Set());
}
}, [showScanner]);
// Closing the scanner forgets what it saw, so reopening can scan the same
// code again.
const [scannerWasOpen, setScannerWasOpen] = useState(showScanner);
if (showScanner !== scannerWasOpen) {
setScannerWasOpen(showScanner);
if (!showScanner) setScannedCodes(new Set());
}

const handleScan = async (detectedCodes: { rawValue: string }[]) => {
if (isProcessing || !detectedCodes || detectedCodes.length === 0) return;
Expand Down
15 changes: 7 additions & 8 deletions sites/mainweb/app/(portal)/hackathons/[id]/judge/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,16 @@ export default function JudgeHackathonPage() {
// Seeds the first project only. Once a mutation has taken over, `current` or
// `done` is set and a late-arriving response must not replace what the judge
// is looking at.
useEffect(() => {
if (!nextTable.data || current || done) return;
if (nextTable.data && !current && !done) {
if (nextTable.data.done) {
setDone(true);
return;
} else {
setCurrent({
project: (nextTable.data.project as Project) ?? null,
queueId: nextTable.data.queueId ?? null,
});
}
setCurrent({
project: (nextTable.data.project as Project) ?? null,
queueId: nextTable.data.queueId ?? null,
});
}, [nextTable.data, current, done]);
}

const advance = (
project: Project | null,
Expand Down
7 changes: 3 additions & 4 deletions sites/mainweb/app/(portal)/judge/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";

import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { trpc } from "@/lib/trpc";
import { useIsClient } from "@/lib/use-is-client";
import { LiquidGlass } from "@/components/portal/LiquidGlass";
import { LoadingScreen } from "@/components/portal/LoadingScreen";
import Link from "next/link";
Expand All @@ -24,7 +25,7 @@ type HackathonData = {
export default function JudgePage() {
const { data: session, status } = useSession();
const router = useRouter();
const [mounted, setMounted] = useState(false);
const mounted = useIsClient();

const { data: judgeStatus, isLoading: checkingJudge } =
trpc.judge.isJudge.useQuery(undefined, { enabled: !!session });
Expand All @@ -41,8 +42,6 @@ export default function JudgePage() {
enabled: !!session,
});

useEffect(() => setMounted(true), []);

// The approval email points here. Rendering nothing for a signed-out visitor
// turned an expired session into a broken link.
useEffect(() => {
Expand Down
4 changes: 2 additions & 2 deletions sites/mainweb/app/(portal)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useSession, signIn, getProviders } from "next-auth/react";
import { useRouter, useSearchParams } from "next/navigation";
import { usePortalContext } from "@/lib/use-portal-context";
import { safeCallback } from "@/lib/safe-callback";
import { useIsClient } from "@/lib/use-is-client";

// DSGT Query - Premium Landing Page
// Ultra-modern, standout UI/UX
Expand All @@ -14,7 +15,7 @@ export default function Home() {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = safeCallback(searchParams.get("callbackUrl"));
const [mounted, setMounted] = useState(false);
const mounted = useIsClient();
const [showEmailInput, setShowEmailInput] = useState(false);
const [email, setEmail] = useState("");
const [emailSending, setEmailSending] = useState(false);
Expand All @@ -37,7 +38,6 @@ export default function Home() {
);

useEffect(() => {
setMounted(true);
getProviders()
.then((p) => setProviders(p ?? {}))
// A failed lookup must not hide every sign-in button. Falling back to
Expand Down
29 changes: 15 additions & 14 deletions sites/mainweb/app/(portal)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ import { ResumeSection } from "@/components/portal/ResumeSection";
import Image from "next/image";
import { trpc } from "@/lib/trpc";
import { trpcErrorMessage } from "@/lib/trpc-error";
import { useIsClient } from "@/lib/use-is-client";
import { LiquidGlass } from "@/components/portal/LiquidGlass";
import { LoadingScreen } from "@/components/portal/LoadingScreen";

export default function SettingsPage() {
const { data: session, status } = useSession();
const router = useRouter();
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const mounted = useIsClient();
const [activeTab, setActiveTab] = useState<
"profile" | "membership" | "appearance" | "account"
>("profile");
Expand Down Expand Up @@ -123,23 +124,23 @@ export default function SettingsPage() {
gtEmail: "",
});

useEffect(() => { setMounted(true); }, []);

useEffect(() => {
if (status === "unauthenticated") router.push("/login");
}, [status, router]);

useEffect(() => {
if (userData) {
setForm({
name: userData.name || "",
bio: userData.bio || "",
website: userData.website || "",
location: userData.location || "",
gtEmail: userData.gtEmail || "",
});
}
}, [userData]);
// Refill the form whenever new user data arrives. Done during render rather
// than in an effect, so the stale form never paints.
const [filledFrom, setFilledFrom] = useState<typeof userData>(undefined);
if (userData && userData !== filledFrom) {
setFilledFrom(userData);
setForm({
name: userData.name || "",
bio: userData.bio || "",
website: userData.website || "",
location: userData.location || "",
gtEmail: userData.gtEmail || "",
});
}

const handleSave = async () => {
setIsSaving(true);
Expand Down
31 changes: 12 additions & 19 deletions sites/mainweb/app/(portal)/submit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useSession } from "next-auth/react";
import { trpc } from "@/lib/trpc";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { Suspense, useState } from "react";
import { LiquidGlass } from "@/components/portal/LiquidGlass";
import { LoadingScreen } from "@/components/portal/LoadingScreen";
import Link from "next/link";
Expand Down Expand Up @@ -191,24 +191,22 @@ function SubmitPortalContent() {
onError: (err) => setError(err.message),
});

useEffect(() => {
if (!selectedHackathonId && myRegs && myRegs.length > 0) {
const firstReg = myRegs[0];
if (firstReg) {
setSelectedHackathonId(firstReg.hackathonId);
}
if (!selectedHackathonId && myRegs && myRegs.length > 0) {
const firstReg = myRegs[0];
if (firstReg) {
setSelectedHackathonId(firstReg.hackathonId);
}
}, [myRegs, selectedHackathonId]);
}

// Fill the form from the existing submission, once per selected event.
// Clearing when there is none matters as much as filling: the form used to
// keep the previous event's answers after switching, so a submit could file
// one hackathon's project against another.
useEffect(() => {
if (!selectedHackathonId) return;
if (mySubmission.isPending) return;
if (prefilledFor === selectedHackathonId) return;

if (
selectedHackathonId &&
!mySubmission.isPending &&
prefilledFor !== selectedHackathonId
) {
const p = mySubmission.data;
setProjectName(p?.name ?? "");
setProjectDesc(p?.description ?? "");
Expand All @@ -219,12 +217,7 @@ function SubmitPortalContent() {
setChallenges(p?.challenges ?? []);
setIsCreateX(p?.isCreateX ?? false);
setPrefilledFor(selectedHackathonId);
}, [
selectedHackathonId,
mySubmission.isPending,
mySubmission.data,
prefilledFor,
]);
}

if (status === "loading" || loadingRegs) {
return <LoadingScreen message="Initializing Workspace…" />;
Expand Down
5 changes: 3 additions & 2 deletions sites/mainweb/app/(portal)/verify/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ function VerifyContent() {
// /dashboard by the route, so the `||` below can never fall through to
// anything else — reading the query param first is what actually
// returns somebody to the page that sent them here.
window.location.href =
callbackUrl || data.redirectUrl || "/dashboard";
window.location.assign(
callbackUrl || data.redirectUrl || "/dashboard",
);
} else {
setError(data.error || "Invalid code. Please try again.");
setVerifying(false);
Expand Down
Loading
Loading