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
9 changes: 5 additions & 4 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ const eslintConfig = [
...(reactHooksPlugin
? [
{
// react-hooks/set-state-in-effect is new in eslint-plugin-react-hooks@7
// and flags the async `load()` pattern used throughout the codebase.
// Downgraded to warn here; callers should be refactored in a follow-up.
// react-hooks/set-state-in-effect and incompatible-library flag common
// patterns here (async load() in effects, react-hook-form watch()).
// Disabled until refactored in a follow-up.
plugins: { "react-hooks": reactHooksPlugin },
rules: {
"react-hooks/set-state-in-effect": "warn",
"react-hooks/set-state-in-effect": "off",
"react-hooks/incompatible-library": "off",
},
},
]
Expand Down
4 changes: 2 additions & 2 deletions scripts/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ async function main() {
if (!quarter) {
quarter = db
.insert(stfQuarter)
.values({ name: "Fall 2025", isActive: true })
.values({ name: "2025-2026", isActive: true })
.returning()
.get();
console.log(`Seeded active quarter: ${quarter.name}`);
console.log(`Seeded active school year: ${quarter.name}`);
}

const defaultBuckets = [
Expand Down
2 changes: 1 addition & 1 deletion scripts/verify-order-export-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const admin = db.select().from(user).limit(1).get();
assert(admin != null, "Need a user");

const quarter = getActiveQuarter();
assert(quarter != null, "Need active quarter");
assert(quarter != null, "Need active school year");

const mechanical = db.select().from(stfBucket).where(eq(stfBucket.name, "Mechanical")).get();
assert(mechanical != null, "Need Mechanical bucket");
Expand Down
2 changes: 1 addition & 1 deletion scripts/verify-order-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const stfBalance = validateOrderBalance("STF", stfData.stfBucketId, stfTotal);
assert(stfBalance.ok, "STF balance check should pass before insert");

const quarter = getActiveQuarter();
assert(quarter != null, "Active quarter required");
assert(quarter != null, "Active school year required");

const stfOrder = db
.insert(order)
Expand Down
2 changes: 1 addition & 1 deletion src/app/(dashboard)/admin/finance/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default async function AdminFinancePage() {
<div>
<h1 className="text-3xl">Finance</h1>
<p className="text-muted-foreground">
Manage STF buckets, gift fund value, and quarterly resets.
Manage STF buckets, gift fund value, and school year resets.
</p>
</div>

Expand Down
6 changes: 3 additions & 3 deletions src/app/api/admin/finance/buckets/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function POST(req: NextRequest) {
const quarter = getActiveQuarter();
if (!quarter) {
return NextResponse.json(
{ error: "No active STF quarter. Create one before adding buckets." },
{ error: "No active STF school year. Create one before adding buckets." },
{ status: 400 }
);
}
Expand All @@ -49,13 +49,13 @@ export async function PUT(req: NextRequest) {
const body = await req.json().catch(() => null);
const quarterName = body?.quarterName?.trim();
if (!quarterName) {
return NextResponse.json({ error: "Quarter name is required" }, { status: 400 });
return NextResponse.json({ error: "School year name is required" }, { status: 400 });
}

const existing = getActiveQuarter();
if (existing) {
return NextResponse.json(
{ error: `Active quarter already exists: ${existing.name}` },
{ error: `Active school year already exists: ${existing.name}` },
{ status: 400 }
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/admin/finance/quarter-reset/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ export async function POST(req: NextRequest) {

const active = getActiveQuarter();
if (!active) {
return NextResponse.json({ error: "No active quarter to reset" }, { status: 400 });
return NextResponse.json({ error: "No active school year to reset" }, { status: 400 });
}

if (active.name !== parsed.data.quarterName) {
return NextResponse.json(
{ error: `Quarter name does not match. Expected "${active.name}".` },
{ error: `School year name does not match. Expected "${active.name}".` },
{ status: 400 }
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/orders/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id
const activeQuarter = d.fundType === "STF" ? getActiveQuarter() : null;
if (d.fundType === "STF" && !activeQuarter) {
return NextResponse.json(
{ error: "No active STF quarter is configured. Contact an officer." },
{ error: "No active STF school year is configured. Contact an officer." },
{ status: 400 }
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/orders/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function POST(req: NextRequest) {
const activeQuarter = d.fundType === "STF" ? getActiveQuarter() : null;
if (d.fundType === "STF" && !activeQuarter) {
return NextResponse.json(
{ error: "No active STF quarter is configured. Contact an officer." },
{ error: "No active STF school year is configured. Contact an officer." },
{ status: 400 }
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/BalanceAmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function RemainingBalanceCaption({ cents, className }: RemainingBalanceCa
className
)}
>
{over ? "Over budget this quarter" : "remaining this quarter"}
{over ? "Over budget this school year" : "remaining this school year"}
</p>
);
}
6 changes: 3 additions & 3 deletions src/components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useForm, useWatch } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

Expand Down Expand Up @@ -105,7 +105,7 @@ export function LoginForm({ notice }: { notice?: string }) {
resolver: zodResolver(credentialsSchema),
defaultValues: { name: "", email: "", password: "", confirmPassword: "" },
});
const watchedPassword = credForm.watch("password");
const watchedPassword = useWatch({ control: credForm.control, name: "password" });
const otpForm = useForm<OtpValues>({
resolver: zodResolver(otpSchema),
defaultValues: { otp: "" },
Expand Down Expand Up @@ -238,7 +238,7 @@ export function LoginForm({ notice }: { notice?: string }) {
}

toast.success("Email verified - welcome!");
window.location.href = "/dashboard";
router.replace("/dashboard");
}

async function onForgotSubmit(values: ForgotValues) {
Expand Down
8 changes: 6 additions & 2 deletions src/components/dashboard/LiveClock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ export function LiveClock() {

return (
<div className="text-right">
<p className="font-heading text-3xl text-white tabular-nums">{time}</p>
<p className="text-muted-foreground text-sm">{date}</p>
<p suppressHydrationWarning className="font-heading text-3xl text-white tabular-nums">
{time}
</p>
<p suppressHydrationWarning className="text-muted-foreground text-sm">
{date}
</p>
</div>
);
}
9 changes: 3 additions & 6 deletions src/components/dashboard/MinecraftStatusTile.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";

import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";

import { CardContent, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import type { ServerStatus } from "@/lib/minecraft";
import { usePoll } from "@/lib/use-poll";

function PingDot({ online }: { online: boolean }) {
if (!online) return <span className="bg-muted-foreground/40 size-2.5 rounded-full" />;
Expand All @@ -28,11 +29,7 @@ export function MinecraftStatusTile() {
}
}, []);

useEffect(() => {
load();
const id = setInterval(load, 30_000);
return () => clearInterval(id);
}, [load]);
usePoll(load, 30_000);

if (!status) {
return (
Expand Down
9 changes: 3 additions & 6 deletions src/components/dashboard/NetworkStatusTile.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";

import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";

import { CardContent, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { usePoll } from "@/lib/use-poll";

type NetworkStatus = {
online: boolean;
Expand Down Expand Up @@ -34,11 +35,7 @@ export function NetworkStatusTile() {
}
}, []);

useEffect(() => {
load();
const id = setInterval(load, 30_000);
return () => clearInterval(id);
}, [load]);
usePoll(load, 30_000);

if (!status) {
return (
Expand Down
9 changes: 3 additions & 6 deletions src/components/dashboard/SystemVitals.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"use client";

import { Cpu, HardDrive, MemoryStick, Timer } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import type { SystemStats } from "@/app/api/system/stats/route";
import { usePoll } from "@/lib/use-poll";

function metricColor(pct: number): string {
if (pct > 85) return "var(--destructive)";
Expand Down Expand Up @@ -71,11 +72,7 @@ export function SystemVitals() {
}
}, []);

useEffect(() => {
load();
const id = setInterval(load, 10_000);
return () => clearInterval(id);
}, [load]);
usePoll(load, 10_000);

if (!stats) {
return (
Expand Down
32 changes: 16 additions & 16 deletions src/components/finance/FinanceManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ export function FinanceManager({ initial }: { initial: FinanceData }) {
});
if (!res.ok) {
const err = await res.json().catch(() => null);
throw new Error(err?.error ?? "Failed to create quarter");
throw new Error(err?.error ?? "Failed to create school year");
}
toast.success("Quarter created");
toast.success("School year created");
setNewQuarterName("");
await refresh();
} catch (err) {
Expand Down Expand Up @@ -196,7 +196,7 @@ export function FinanceManager({ initial }: { initial: FinanceData }) {
const err = await res.json().catch(() => null);
throw new Error(err?.error ?? "Reset failed");
}
toast.success("Quarter reset complete");
toast.success("School year reset complete");
setResetStep(0);
setResetConfirmName("");
setNextQuarterName("");
Expand All @@ -212,29 +212,29 @@ export function FinanceManager({ initial }: { initial: FinanceData }) {
<div className="space-y-10">
<section className="space-y-4">
<div>
<h2 className="text-lg font-semibold">STF quarter</h2>
<h2 className="text-lg font-semibold">STF school year</h2>
<p className="text-muted-foreground text-sm">
Active quarter: {activeQuarter?.name ?? "None"}
Active school year: {activeQuarter?.name ?? "None"}
</p>
</div>
{!activeQuarter ? (
<div className="flex flex-wrap items-end gap-3">
<div className="space-y-1">
<Label htmlFor="new-quarter">Quarter name</Label>
<Label htmlFor="new-quarter">School year</Label>
<Input
id="new-quarter"
placeholder="Fall 2025"
placeholder="2025-2026"
value={newQuarterName}
onChange={(e) => setNewQuarterName(e.target.value)}
/>
</div>
<Button onClick={createQuarter} disabled={busy === "quarter"}>
Create quarter
Create school year
</Button>
</div>
) : (
<Button variant="destructive" onClick={() => setResetStep(1)}>
Reset quarter
Reset school year
</Button>
)}
</section>
Expand Down Expand Up @@ -386,12 +386,12 @@ export function FinanceManager({ initial }: { initial: FinanceData }) {
{resetStep === 1 ? (
<>
<DialogHeader>
<DialogTitle>Reset quarter?</DialogTitle>
<DialogTitle>Reset school year?</DialogTitle>
<DialogDescription>
This will archive all STF buckets for{" "}
<strong>{activeQuarter?.name}</strong> and clear them for the
new quarter. Gift fund orders and value are not affected. This
cannot be undone.
new school year. Gift fund orders and value are not affected.
This cannot be undone.
</DialogDescription>
</DialogHeader>
<div className="flex justify-end gap-2">
Expand All @@ -409,23 +409,23 @@ export function FinanceManager({ initial }: { initial: FinanceData }) {
<DialogTitle>Confirm reset</DialogTitle>
<DialogDescription>
Type <strong>{activeQuarter?.name}</strong> to confirm, then
enter the name for the incoming quarter.
enter the name for the incoming school year.
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="space-y-1">
<Label htmlFor="confirm-quarter">Current quarter name</Label>
<Label htmlFor="confirm-quarter">Current school year</Label>
<Input
id="confirm-quarter"
value={resetConfirmName}
onChange={(e) => setResetConfirmName(e.target.value)}
/>
</div>
<div className="space-y-1">
<Label htmlFor="next-quarter">New quarter name</Label>
<Label htmlFor="next-quarter">New school year</Label>
<Input
id="next-quarter"
placeholder="Winter 2026"
placeholder="2026-2027"
value={nextQuarterName}
onChange={(e) => setNextQuarterName(e.target.value)}
/>
Expand Down
9 changes: 3 additions & 6 deletions src/components/github/AdminGithubManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
GithubTeam,
GithubTeamMember,
} from "@/lib/github";
import { usePoll } from "@/lib/use-poll";
import { formatDate } from "@/lib/utils";

type Tab = "members" | "invitations" | "teams";
Expand Down Expand Up @@ -53,9 +54,7 @@ function MembersTab() {
}
}, []);

useEffect(() => {
load();
}, [load]);
usePoll(load);

async function addMember() {
const value = invitee.trim();
Expand Down Expand Up @@ -219,9 +218,7 @@ function InvitationsTab() {
}
}, []);

useEffect(() => {
load();
}, [load]);
usePoll(load);

async function cancel(id: number) {
setBusy(id);
Expand Down
Loading