Skip to content
Open
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
19 changes: 0 additions & 19 deletions .eslintrc.json

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ next-env.d.ts

# Claude Code local settings (per-machine, never committed)
.claude/
.npmrc
8 changes: 7 additions & 1 deletion app/(app)/organizer/hackathons/[slug]/media/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@ export default async function EventMediaPage({
if (!membership || membership.role === "MEMBER") notFound();

const deadline = event.mediaDeadlineAt;
// Server Component: this runs once per server request, not a client
// re-render, so Date.now() is the intended per-request deadline check
// (react-hooks/purity is new in eslint-config-next 16 and doesn't
// distinguish RSC render from client render).
const hoursLeft = deadline
? Math.round((deadline.getTime() - Date.now()) / 3_600_000)
? // eslint-disable-next-line react-hooks/purity -- see comment above
Math.round((deadline.getTime() - Date.now()) / 3_600_000)
: null;
// eslint-disable-next-line react-hooks/purity -- see comment above
const pastDeadline = deadline ? deadline.getTime() < Date.now() : false;
const approvedCount = event.media.filter((asset) => asset.status === "APPROVED").length;

Expand Down
4 changes: 4 additions & 0 deletions app/(auth)/invites/[token]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export default async function InvitePage({ params }: { params: Promise<{ token:
if (
!invitation ||
invitation.acceptedAt ||
// Server Component: runs once per server request, so Date.now() is the
// intended per-request expiry check (react-hooks/purity is new in
// eslint-config-next 16 and doesn't distinguish RSC render from client).
// eslint-disable-next-line react-hooks/purity -- see comment above
invitation.expiresAt.getTime() <= Date.now()
) {
return <AcceptInvitationClient invalid orgName={invitation?.org.name} />;
Expand Down
2 changes: 1 addition & 1 deletion app/(marketing)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default async function BlogPostPage({ params }: PageProps) {
src={meta.cover}
alt={meta.coverAlt}
fill
priority
preload
sizes="(min-width: 1440px) 1392px, 100vw"
className="object-cover"
/>
Expand Down
2 changes: 1 addition & 1 deletion app/(marketing)/hackathons/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export default async function EventDetailPage({ params, searchParams }: PageProp
src={cover}
alt={`${event.title} hackathon cover`}
fill
priority
preload
sizes="(min-width: 1024px) 380px, 100vw"
unoptimized={!cover.startsWith("/")}
className="object-cover"
Expand Down
5 changes: 4 additions & 1 deletion components/auth/oauth-popup-pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ export function OAuthPopupDone() {
const [orphaned, setOrphaned] = useState(false);
useEffect(() => {
if (!reportToOpener({ type: OAUTH_POPUP_NAME, status: "success" })) {
// Opened outside the popup flow (or the original tab was closed): carry on here.
// Opened outside the popup flow (or the original tab was closed): carry
// on here. This relays a one-time external result, not reactive state
// (react-hooks/set-state-in-effect is new in eslint-config-next 16).
// eslint-disable-next-line react-hooks/set-state-in-effect -- see comment above
setOrphaned(true);
window.location.replace("/dashboard");
}
Expand Down
2 changes: 1 addition & 1 deletion components/hackathons/hackathon-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function HackathonCover({
// Uploaded covers live on R2 and are already sized to 1600x900.
unoptimized={!cover.startsWith("/")}
alt={`${event.title}, a hackathon on HackVillage`}
priority={priority}
preload={priority}
fill
sizes={sizes}
className={cn(
Expand Down
4 changes: 4 additions & 0 deletions components/judging/judging-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export async function JudgingSection({
status: EventStatus;
}) {
const readiness = await judgingReadiness(eventId);
// Server Component: runs once per server request, so Date.now() is the
// intended per-request "has the event ended" check (react-hooks/purity is
// new in eslint-config-next 16 and doesn't distinguish RSC from client).
// eslint-disable-next-line react-hooks/purity -- see comment above
const ended = endsAt.getTime() <= Date.now();
const judging = status === "JUDGING";

Expand Down
4 changes: 4 additions & 0 deletions components/patterns/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ export function AppTopBar({
const [mounted, setMounted] = useState(false);

useEffect(() => {
// Hydration-safe mount flag: intentionally renders differently once the
// client has mounted (react-hooks/set-state-in-effect is new in
// eslint-config-next 16; this is the standard pattern for it).
// eslint-disable-next-line react-hooks/set-state-in-effect -- see comment above
setMounted(true);
}, []);

Expand Down
2 changes: 1 addition & 1 deletion components/patterns/auth-visual.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function AuthVisual() {
src="/marketing/auth/hackathon-builders-welcome.webp"
alt=""
fill
priority
preload
sizes="50vw"
className="object-cover object-top"
/>
Expand Down
4 changes: 4 additions & 0 deletions components/patterns/legal-toc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export function LegalToc({ containerId }: { containerId: string }) {
headings.forEach((heading, index) => {
heading.id = ids[index];
});
// Builds the TOC from headings read out of the DOM after mount, which
// only exists client-side (react-hooks/set-state-in-effect is new in
// eslint-config-next 16 and flags this DOM-read-then-setState pattern).
// eslint-disable-next-line react-hooks/set-state-in-effect -- see comment above
setEntries(
headings.map((heading, index) => ({
id: ids[index],
Expand Down
4 changes: 4 additions & 0 deletions components/patterns/mobile-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export function MobileNav({ navLinks, organizerLinks, trailingLinks }: MobileNav
const [mounted, setMounted] = useState(false);

useEffect(() => {
// Hydration-safe mount flag: intentionally renders differently once the
// client has mounted (react-hooks/set-state-in-effect is new in
// eslint-config-next 16; this is the standard pattern for it).
// eslint-disable-next-line react-hooks/set-state-in-effect -- see comment above
setMounted(true);
}, []);

Expand Down
39 changes: 39 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";

export default defineConfig([
...nextVitals,
...nextTs,
globalIgnores([
".next/**",
"out/**",
"build/**",
"coverage/**",
"next-env.d.ts",
// Hardhat output (contracts carry their own toolchain).
"artifacts/**",
"cache/**",
"typechain-types/**",
"public/uploads/**",
]),
{
rules: {
// Escrow/payout service boundaries are hard (ADR-001): vendor SDKs are
// only reachable through lib/ports. Kept as a warning until the boundary
// import lint rule lands in Phase 3.
"no-restricted-imports": [
"warn",
{
patterns: [
{
group: ["ethers"],
message:
"Vendor SDKs must be accessed via lib/ports — never imported directly (ADR-001).",
},
],
},
],
},
},
]);
5 changes: 2 additions & 3 deletions lib/auth/guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import { canAccessSurface, isOnboarded } from "@/lib/auth/rbac";

/**
* Server-side guards (Phase 1). Layouts and server actions call these —
* enforcement NEVER lives in the client. Middleware is deliberately unused:
* the session check reads Prisma (revocation, roles), which doesn't belong in
* middleware; the layout tree is the single, stronger gate.
* enforcement NEVER lives in the client. proxy.ts only refreshes sessions and
* never decides access: the layout tree is the single, stronger gate.
*/

export interface CurrentUser extends SessionUserLike {
Expand Down
2 changes: 1 addition & 1 deletion lib/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
// jwt callback ends any session whose version is stale or whose account is
// gone (ADR-005 amended; see lib/auth/session-version.ts).
// The cookie lives as long as the longest inactivity window and slides on
// every request (middleware.ts refreshes it). The exact per-role idle and
// every request (proxy.ts refreshes it). The exact per-role idle and
// overall limits are enforced in the jwt callback (lib/auth/session-policy).
session: { strategy: "jwt", maxAge: MAX_IDLE_SECONDS },
pages: { signIn: "/signin", error: "/signin" },
Expand Down
2 changes: 1 addition & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ function sentryIngestOrigin(): string | null {
* keeps 'unsafe-inline' because the App Router streams its RSC payload in
* inline <script> tags (self.__next_f) and app/layout.tsx ships an inline
* theme-init script — strict 'self' would blank every page without a nonce
* pipeline (deliberately not added: no middleware). JSON-LD <script> tags are
* pipeline (deliberately not added: proxy.ts only refreshes sessions). JSON-LD <script> tags are
* type="application/ld+json" and execute nowhere, so they are unaffected.
*/
function securityHeaders(): Array<{ key: string; value: string }> {
Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"dev:clean": "rm -rf .next && next dev --turbopack",
"build": "prisma generate --schema db/schema.prisma && node scripts/migrate-if-production.mjs && next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "prisma generate --schema db/schema.prisma && tsc --noEmit",
Expand All @@ -40,13 +40,13 @@
"@node-rs/argon2": "^2.2.1",
"@openzeppelin/contracts": "^5.6.1",
"@prisma/client": "^6.0.0",
"@sentry/nextjs": "^11.0.0",
"@sentry/nextjs": "^11.4.0",
"bowser": "^2.14.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"ethers": "^6.17.0",
"lucide-react": "^0.475.0",
"next": "^15.0.0",
"next": "^16.3.8",
"next-auth": "^5.0.0-beta.32",
"pg-boss": "^10.1.5",
"react": "^19.0.0",
Expand All @@ -64,10 +64,10 @@
"@types/node": "^22.10.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitest/coverage-v8": "^3.2.7",
"@vitest/coverage-v8": "^4.1.11",
"chai": "^4.5.0",
"eslint": "^8.57.1",
"eslint-config-next": "^15.0.0",
"eslint": "^9.39.5",
"eslint-config-next": "^16.3.8",
"hardhat": "^2.29.1",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.10",
Expand All @@ -76,6 +76,6 @@
"ts-node": "^10.9.2",
"tsx": "^4.19.2",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
"vitest": "^4.1.11"
}
}
Loading