Skip to content

[#380] Update package versions#383

Merged
DVidal1205 merged 28 commits intomainfrom
update-package-versions
Mar 1, 2026
Merged

[#380] Update package versions#383
DVidal1205 merged 28 commits intomainfrom
update-package-versions

Conversation

@Spyderma9
Copy link
Contributor

@Spyderma9 Spyderma9 commented Feb 27, 2026

Why

All of our dependencies were really behind, so updating them was important for performance improvements, security patches, etc

What

Closes: #380

Bumped all outdated dependencies across the monorepo catalog and individual packages. Major version upgrades: Next.js 14 → 16, React 18 → 19, Tailwind CSS v3 → v4, ESLint 9 → 10, Zod 3 → 4, tRPC 11.0-rc → 11.10.0 (stable), @tanstack/react-query 5.59 → 5.90, react-day-picker 8 → 9, drizzle-zod 0.5.1 → 0.8.3, and rsuite 5 → 6.

Test Plan

Ran pnpm install successfully (exit 0)
Ran pnpm build successfully — all 13/13 tasks passed (exit 0)
Ran pnpm dev and verified all apps load and render correctly on their respective ports
Visually confirmed all pages are identical to pre-update appearance

Checklist

  • Database: No schema changes, OR I have contacted the Development Lead to run db:push before merging
  • Environment Variables: No environment variables changed, OR I have contacted the Development Lead to modify them on Coolify BEFORE merging.

Summary by CodeRabbit

  • New Features

    • Upgraded core stack to React 19 / Next.js 16 across apps.
  • Improvements

    • Richer dark-mode colors, new UI animations, and updated responsive visuals (SVGs, spacing, sizing).
    • Charts and admin dashboards adjusted for improved layout and tooltip stability.
    • Forms: tightened input defaults, clearer validation payloads, and smoother file-size handling.
  • Bug Fixes

    • Restored build-time lint enforcement so lint issues can now fail builds.
  • Chores

    • Wide dependency and tooling upgrades for compatibility and stability.

- Bump eslint from ^9.12.0 to ^9.22.0
- Upgrade tailwindcss from ^3.4.14 to ^4.2.1
- Update React and related types from version 18.x to 19.x
- Upgrade @next/eslint-plugin-next from ^14.2.15 to ^15.5.12
- Add @tailwindcss/postcss as a dependency
- Remove tailwindcss-animate plugin from Tailwind configuration
Updates tRPC to 11.10, TanStack Query to 5.90.21, superjson to 2.2.6, cmdk to 1.1.1, @auth/drizzle-adapter to 1.11.1, and runs pnpm update -r to pull all other packages to their latest compatible versions.
…→6, @react-email/render 1→2

- next@16.1.6 across all apps and packages/auth
- eslint-config-next@16 in apps/2025
- stripe@20.4.0: remove deprecated { typescript: true } constructor option
- drizzle-kit@0.31.9 in packages/db
- resend@6.0.0 in packages/api, apps/tk, apps/cron
- @react-email/render@2.0.0 in apps/blade

Fix Next.js 16 / Turbopack 1.0 breaking changes:
- tailwind.config.ts: switch from named to default import for
  tailwindcss/defaultTheme (fontFamily no longer a named export in v4)
- next.config.js: remove deprecated eslint.ignoreDuringBuilds key
  (Next.js 16 no longer runs ESLint during builds)
- Upgraded @eslint/compat from ^1.4.1 to ^2.0.2
- Upgraded @next/eslint-plugin-next from ^15.5.12 to ^16.1.6
- Upgraded eslint-plugin-react-hooks from ^5.2.0 to ^7.0.1
- Upgraded prettier-plugin-tailwindcss from ^0.6.14 to ^0.7.2
- @stripe/react-stripe-js 3→5, @stripe/stripe-js 5→8
- @hookform/resolvers 3→5 (simplify useForm generic for v5 zod overloads)
- date-fns 3→4, sonner 1→2, react-markdown 9→10
- recharts 2→3 (rewrite chart.tsx prop types removed in v3 API)
- google-auth-library 9→10, googleapis 144→171
  (convert JWT constructor from positional args to options object)
- node-cron 3→4
- jimp 0.22→1 (default export → named export { Jimp })
- Replace deprecated legacyBehavior/passHref <Link> pattern with modern
  Next.js <Link> usage in apps/2025 (sponsorPosters.tsx, partnerPosters.tsx)
- Modernize Tailwind v4 class names across apps/club (9 files), apps/blade,
  and apps/2025: bg-gradient-* → bg-linear-*, [&_*]: → **, arbitrary px
  values → named shorthands, mask-image syntax update
- Fix conflicting utility classes in club (text-transparent/text-white,
  block/hidden)
- Fix JSX SVG attribute casing in apps/gemiknights (fill-rule → fillRule,
  clip-rule → clipRule, stop-color → stopColor, stroke-width → strokeWidth)
@Spyderma9 Spyderma9 self-assigned this Feb 27, 2026
@Spyderma9 Spyderma9 requested a review from a team as a code owner February 27, 2026 06:56
@Spyderma9 Spyderma9 added Major Big change - 2+ reviewers required Global Change modifies code for the entire repository dependencies Pull requests that update a dependency file labels Feb 27, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Monorepo-wide upgrades and compatibility changes: dependency bumps (Next 16, React 19, Tailwind, Recharts, Stripe, Jimp), Tailwind/PostCSS rewiring, many JSX SVG prop fixes, async route params/headers/cookies now awaited, Recharts typing workarounds, UI class and public API typing adjustments. No large new features added.

Changes

Cohort / File(s) Summary
Workspace & root
package.json, pnpm-workspace.yaml, turbo.json
Tooling scripts and workspace overrides updated; turbo/@turbo/gen bumped; PNPM overrides set React/Types to v19.
Dependency bumps (apps & packages)
apps/*/package.json, packages/*/package.json, tooling/*/package.json, packages/ui/package.json
Mass upgrade of core libs: Next → 16, React → 19, Tailwind, Recharts v3, Stripe v20, Jimp v1+, many @radix/@types bumps.
Next.js configs
apps/*/next.config.js (multiple)
Removed eslint: { ignoreDuringBuilds: true } across apps — builds no longer bypass ESLint.
Tailwind / PostCSS & globals
apps/*/postcss.config.cjs, apps/*/tailwind.config.ts, apps/*/globals.css, tooling/tailwind/*
Switched PostCSS plugin key to @tailwindcss/postcss, replaced @tailwind directives with module imports, rewrote tailwind configs (dark mode, new color tokens, animations), added container utility and responsive variants.
Async params / headers / cookies
many apps/*/src/app/**/page.tsx, apps/*/src/trpc/server.ts, packages/auth/src/*, packages/api/src/routers/judge.ts, packages/api/src/utils.ts
Route/page component signatures changed to accept props with Promise-based params/searchParams and now await them; headers() and cookies() are awaited where used; createContext awaits headers.
SVG JSX normalization
apps/club/src/app/_components/links/assets/*, apps/gemiknights/src/app/_components/graphics/*, etc.
Converted SVG attributes from kebab-case to React camelCase (fill-rulefillRule, stroke-widthstrokeWidth, color-interpolation-filterscolorInterpolationFilters).
Recharts & chart typing fixes
apps/blade/src/app/_components/admin/charts/*.tsx, packages/ui/src/chart.tsx
Added // @ts-expect-error`` for removed activeIndex type, added payload guards, and updated tooltip/legend prop types to match Recharts v3.
Forms / Zod / validation
apps/blade/..., packages/api/src/routers/forms.ts, packages/api/src/routers/email.ts
Zod error shape adjustments (typeorigin for too_big), schema transform typing workarounds, email input schema tightened to record<string,string>, response error mapping uses issues.
UI class and presentation tweaks
many apps/*/src/** and packages/ui/src/*
Replaced arbitrary Tailwind values with presets, reordered utility class strings, adjusted sizes (max-w/min-h), removed redundant props (e.g., Bar layout), minor DOM structure tweaks.
Next Link modernization
apps/2025/src/app/_components/*
Replaced legacy Link+inner <a> (passHref/legacyBehavior) with modern Link usage and moved attributes onto Link.
Stripe & Jimp init changes
packages/api/src/utils.ts, packages/api/src/routers/dues-payment.ts, apps/tk/src/commands/*.ts
Removed { typescript: true } option from Stripe instantiations; switched Jimp imports to import { Jimp as JIMP } and awaited reads using destructured width/height.
Public API / component signature changes
packages/ui/src/form.tsx, packages/ui/src/calendar.tsx, packages/ui/src/chart.tsx, packages/ui/src/select.tsx, apps/blade/src/app/admin/forms/[slug]/page.tsx, etc.
Simplified useForm generics; calendar classNames/Components keys renamed (added Chevron); chart tooltip/legend types updated; SelectTrigger gained suppressHydrationWarning; several page components' param types/signatures updated to Promise-based props.
Lint-suppress and minor logic
many apps/blade/*, apps/gemiknights/*, apps/*
Added/removed inline ESLint disables (set-state-in-effect, no-unnecessary-condition, etc.), small refactors and formatting adjustments.
Significant UI component type change
apps/club/src/app/_components/links/button.tsx, packages/ui/src/chart.tsx, packages/ui/src/calendar.tsx
Button component props changed to a union (button vs anchor); ChartTooltipContent/ChartLegendContent public types updated; Calendar className keys and Components API reworked. Review required for public API consumers.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant NextPage as Page Component
    participant Loader as Server/Data Loaders
    participant TRPC as createContext/TRPC
    Client->>NextPage: Request (route with params promise)
    NextPage->>NextPage: await props.params / props.searchParams
    NextPage->>Loader: call data fetchers / API
    Loader->>TRPC: await headers() / await cookies()
    TRPC->>Loader: return context (session, headers)
    Loader->>NextPage: return data
    NextPage->>Client: render/SSR response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Blade, Guild, CRON, T.K, Hack Sites, API, UI

Suggested reviewers

  • alexanderpaolini
  • DGoel1602
  • DVidal1205
🚥 Pre-merge checks | ✅ 6 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
No Typescript Escape Hatches ⚠️ Warning PR introduces 77 TypeScript escape hatches (14 @ts-expect-error, 63 eslint-disable-next-line) indicating deeper type mismatches rather than properly typing upgraded libraries. Address root causes: properly type Recharts payload, create generic helpers for .transform() schemas, refactor set-state-in-effect patterns, and use strict optional chaining for viewBox types.
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly summarizes the main change: updating package versions across the monorepo, and follows the required format with [#380] issue prefix.
Linked Issues check ✅ Passed All coding-related objectives from #380 are satisfied: Next.js 14→16, React 18→19, Tailwind CSS 3→4, ESLint 9→10 are all upgraded; build and runtime verification completed per test plan.
Out of Scope Changes check ✅ Passed All changes directly support dependency updates specified in #380. While JSX SVG attributes were normalized to camelCase (not explicitly mentioned), these are necessary fixes for React compatibility with updated versions.
No Hardcoded Secrets ✅ Passed No hardcoded secrets found. All sensitive credentials properly referenced through environment variables with .env.example following best practices.
Validated Env Access ✅ Passed PR maintains established pattern by using validated env imports for environment variable access without introducing new direct process.env usage outside approved config files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch update-package-versions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
apps/blade/src/app/_components/dashboard/member/member-application-form.tsx (1)

191-195: ⚠️ Potential issue | 🟡 Minor

Use origin: "file" instead of origin: "number" for file size validation.

In Zod v4's too_big issue format, the origin property should reflect the semantic type being validated. While file.size is technically a number, validating file uploads should use origin: "file" to indicate a file-specific constraint. This matches Zod's design where origin describes the validation context (e.g., "string" for string length, "array" for array length, "file" for file properties).

Change both occurrences from origin: "number" to origin: "file".

Also applies to: line 236

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/dashboard/member/member-application-form.tsx`
around lines 191 - 195, The Zod too_big error objects in the member application
resume validation are using origin: "number" but should use origin: "file";
update the two places in member-application-form.tsx where the resume file size
validation constructs a too_big/validation error (look for the object with
properties origin/maximum/inclusive/message and MINIO.MAX_RESUME_SIZE) to set
origin: "file" instead of "number" so the error context correctly reflects a
file-size validation.
packages/ui/src/sheet.tsx (1)

92-92: ⚠️ Potential issue | 🟡 Minor

Pre-existing typo: focus: ring-offset-2 has an errant space.

Not introduced in this PR, but worth fixing while you're here. The space after focus: breaks the variant binding, so ring-offset-2 applies unconditionally instead of only on focus.

🔧 Suggested fix
-          "focus: ring-offset-2 disabled:pointer-events-none",
+          "focus:ring-offset-2 disabled:pointer-events-none",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/sheet.tsx` at line 92, The class list contains a typo: the
token "focus: ring-offset-2 disabled:pointer-events-none" has an extra space
after "focus:" so the variant binding breaks; locate that string in
packages/ui/src/sheet.tsx (e.g., where the Sheet/trigger classNames are defined)
and change "focus: ring-offset-2" to "focus:ring-offset-2" so ring-offset-2 only
applies on focus while keeping "disabled:pointer-events-none" as-is.
apps/club/src/app/_components/landing/sponsors-assets/sponsor-marquee.tsx (1)

53-54: ⚠️ Potential issue | 🟡 Minor

Improve alt text for better accessibility.

While the second <ul> is correctly aria-hidden="true" (since it duplicates content for the infinite scroll animation), the alt attributes could be more descriptive:

  • Line 53: alt="logo" → Use the company name for screen readers
  • Line 69: alt="s" → Appears to be a typo

Since the second list is hidden from assistive tech, focus on fixing the first list. The second can use empty alt (alt="") since it's decorative/duplicate.

♿ Suggested fix
             <Image
                src={`/logos/${logo.name}.svg`}
                height={0}
                width={0}
-               alt={"logo"}
+               alt={`${logo.name} logo`}
                className="xs:w-20 xs:h-20 h-20 w-20 scale-90 rounded-md object-contain sm:h-24 sm:w-24 md:h-28 md:w-28 lg:h-32 lg:w-32 xl:h-40 xl:w-40"
              />

For the aria-hidden duplicate list:

             <Image
                src={`/logos/${logo.name}.svg`}
                height={0}
                width={0}
-               alt={"s"}
+               alt=""
                className="xs:w-20 xs:h-20 h-20 w-20 scale-90 rounded-md object-contain sm:h-24 sm:w-24 md:h-28 md:w-28 lg:h-32 lg:w-32 xl:h-40 xl:w-40"
              />

Also applies to: 69-70

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/club/src/app/_components/landing/sponsors-assets/sponsor-marquee.tsx`
around lines 53 - 54, In the SponsorMarquee component, replace non-descriptive
alt values on the rendered <img> in the visible sponsor list with the sponsor's
actual name (e.g., use the sponsor.name or sponsor.title used in the
sponsors.map that renders the first <ul>), fix the typo alt="s" to the correct
name where that image is part of the accessible list, and set alt="" for images
in the aria-hidden duplicate <ul> (the infinite-scroll copy) so those decorative
duplicates are ignored by assistive tech; ensure you update the JSX that renders
the <img> elements used by the visible list and by the duplicated aria-hidden
list (refer to the sponsor mapping/rendering code inside SponsorMarquee).
apps/guild/src/app/page.tsx (1)

16-29: ⚠️ Potential issue | 🟠 Major

Handle searchParams as string | string[] | undefined to prevent runtime crashes.

The current type only allows string, but Next.js repeats query parameters as arrays—e.g., ?q=hello&q=world produces { q: ["hello", "world"] }. Calling .trim() on an array throws at runtime.

✅ Safer parsing patch
 export default async function GuildPage(props: {
   searchParams: Promise<{
-    q?: string;
-    page?: string;
-    ps?: string;
-    tag?: string;
+    q?: string | string[];
+    page?: string | string[];
+    ps?: string | string[];
+    tag?: string | string[];
   }>;
 }) {
   const searchParams = await props.searchParams;
-  const query = searchParams.q?.trim() ?? undefined;
+  const first = (v: string | string[] | undefined) =>
+    Array.isArray(v) ? v[0] : v;
+
+  const query = first(searchParams.q)?.trim() ?? undefined;
+  const ps = first(searchParams.ps);
+  const pageRaw = first(searchParams.page);
+  const tag = first(searchParams.tag);

   const pageSize: PageSize =
-    PAGE_SIZE_OPTIONS.find((n) => String(n) === searchParams.ps) ??
+    PAGE_SIZE_OPTIONS.find((n) => String(n) === ps) ??
     DEFAULT_PAGE_SIZE;

-  const currentPage = Number(searchParams.page ?? 0);
-  if (Number.isNaN(currentPage) || currentPage < 0) notFound();
+  const currentPage = pageRaw ? Number.parseInt(pageRaw, 10) : 0;
+  if (!Number.isInteger(currentPage) || currentPage < 0) notFound();

   const selectedTag: GUILD.GuildTag | undefined =
-    GUILD.GUILD_TAG_OPTIONS.find((t) => t === searchParams.tag) ?? undefined;
+    GUILD.GUILD_TAG_OPTIONS.find((t) => t === tag) ?? undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/guild/src/app/page.tsx` around lines 16 - 29, The handler assumes
searchParams.q/ps/page are strings and calls .trim() / String(...) directly,
which will throw if Next.js supplies string[]; update the parsing in page.tsx
(around searchParams, query, pageSize, PAGE_SIZE_OPTIONS, DEFAULT_PAGE_SIZE,
currentPage) to first normalize each param to a single string (e.g., if
Array.isArray(value) take value[0]) or undefined, then call .trim() and parse;
do the same safe-normalization for searchParams.ps and searchParams.page before
comparing to PAGE_SIZE_OPTIONS or calling Number(), and keep the existing
NaN/negative check for currentPage.
apps/tk/src/commands/dog.ts (1)

43-49: ⚠️ Potential issue | 🟠 Major

Return a user-facing error message in the catch path.

The catch block currently logs only, so users can end up with a silent failure/timeout. Reply with an ephemeral fallback message when command execution fails.

Proposed fix
   } catch (err: unknown) {
+    const content = "I couldn't fetch a dog right now. Please try again in a moment.";
+    if (interaction.deferred || interaction.replied) {
+      await interaction.followUp({ content, ephemeral: true });
+    } else {
+      await interaction.reply({ content, ephemeral: true });
+    }
     if (err instanceof Error) {
       console.error(err.message);
     } else {
       console.error("An unknown error occurred: ", err);
     }
   }

As per coding guidelines apps/tk/**: Graceful error handling with user-friendly messages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tk/src/commands/dog.ts` around lines 43 - 49, The catch block catching
"err" (catch (err: unknown)) currently only logs the error; update it to also
send a user-facing ephemeral fallback reply (use the command's
interaction/response object used in this command's execute/run handler, e.g.,
interaction.reply or interaction.followUp) with a short friendly message like
"Something went wrong — please try again later" and mark it ephemeral so only
the user sees it; keep the existing console.error logging (include err) and
guard against sending a second reply if a response was already sent (check
interaction.replied or equivalent) before replying.
packages/ui/package.json (1)

68-74: ⚠️ Potential issue | 🟠 Major

Fix misnamed React catalog in pnpm-workspace.yaml.

The react18 catalog actually references React 19.2.4, not React 18. Rename the catalog to avoid confusion and align with the actual versions:

Proposed fix

In pnpm-workspace.yaml, rename the catalog:

catalogs:
-  react18:
+  react19:
    react: 19.2.4
    react-dom: 19.2.4

Then update packages/ui/package.json:

peerDependencies:
-  "react": "catalog:react18",
+  "react": "catalog:react19",

The current naming mismatch can cause confusion for maintainers and doesn't accurately reflect the versions being used.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/package.json` around lines 68 - 74, Update the catalog name used
for React so it accurately reflects the actual version: rename the catalog
identifier currently called "react18" to something like "react19" in
pnpm-workspace.yaml, then update the react peerDependency in package.json (the
"react": "catalog:react18" entry) to reference the new catalog name; ensure the
dependency entry with actual package version ("react": "^19.2.4") and the
peerDependencies entry stay consistent by using the same catalog identifier.
🧹 Nitpick comments (24)
apps/blade/src/app/_components/admin/club/members/update-member.tsx (3)

118-125: Redundant Number() conversion.

After the Zod .transform() runs during validation, values.points is already a number. The explicit Number(values.points) on line 118 is harmless but unnecessary—it's just Number(aNumber).

If you refactor to z.coerce.number(), this entire intermediate variable can be removed. Otherwise, a quick cleanup:

-  const points = Number(values.points);
-
   updateMember.mutate({
     id: member.id,
     firstName: values.firstName,
     lastName: values.lastName,
     email: values.email,
-    points: points,
+    points: values.points,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/admin/club/members/update-member.tsx` around
lines 118 - 125, The explicit Number conversion is redundant because
values.points is already a number after Zod validation; remove the intermediate
points variable and pass values.points directly into updateMember.mutate (or, if
you switch to z.coerce.number(), also remove the Number() call wherever it
appears). Update the call around updateMember.mutate to use points:
values.points (or just points: values.points) and delete the const points =
Number(values.points) line.

202-218: Consider adding type="number" for the points input.

A text input works but provides a suboptimal mobile experience (no numeric keyboard) and allows non-numeric entry that only fails at validation time. Using type="number" gives users immediate feedback and the appropriate keyboard on mobile devices.

   <FormControl>
-    <Input placeholder="1000" {...field} />
+    <Input type="number" placeholder="1000" {...field} />
   </FormControl>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/admin/club/members/update-member.tsx` around
lines 202 - 218, The Points input should use a numeric input type to improve
mobile UX and prevent non-numeric entry: update the FormField rendering for
name="points" so the Input (inside FormControl) has type="number" (optionally
add inputMode="numeric" or a pattern if desired) and ensure the existing
{...field} spread remains so value/onChange bindings in the
FormField/FormControl flow are preserved; also verify any form parsing (e.g., in
the submit handler or validation schema referenced by form) still converts the
field to a number if required.

82-83: Flagged: @ts-expect-error usage violates coding guidelines.

The comment accurately explains why the suppression is needed (Zod .transform() makes input≠output), but there's a cleaner path that avoids the escape hatch entirely.

Since this PR upgrades to Zod 4, consider using z.coerce.number() instead of z.string().transform(...). Coercion schemas have unified input/output types, eliminating the type mismatch that forces this suppression.

♻️ Refactor to eliminate `@ts-expect-error`
   }).extend({
     firstName: z.string().min(1, "Required"),
     lastName: z.string().min(1, "Required"),
     email: z.string().email("Invalid email").min(1, "Required"),
-    points: z.string().transform((points) => Number(points)),
+    points: z.coerce.number().int().min(0),
     phoneNumber: z
       .string()
       .regex(/^\d{10}|\d{3}-\d{3}-\d{4}$|^$/, "Invalid phone number"),
   });

   const form = useForm({
-    // `@ts-expect-error` -- schema uses .transform() so input≠output; ZodType<TIn,TIn> in useForm can't represent this
     schema: UpdateMemberSchema,

Then on the input field (line 212), add type="number" for better UX:

-  <Input placeholder="1000" {...field} />
+  <Input type="number" placeholder="1000" {...field} />

And remove the now-redundant conversion (line 118):

-  const points = Number(values.points);
   updateMember.mutate({
     id: member.id,
     ...
-    points: points,
+    points: values.points,

As per coding guidelines: **/*.{ts,tsx} - Flag usage of @ts-expect-error; suggest proper typing instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/admin/club/members/update-member.tsx` around
lines 82 - 83, Replace the `@ts-expect-error` suppression by changing the numeric
field in UpdateMemberSchema from z.string().transform(...) to z.coerce.number()
so input/output types match Zod v4; then remove the manual conversion code that
casts the field (the explicit Number/parseInt conversion near where the form
values are processed) and update the corresponding input component to include
type="number" (the input referenced in the form where the member age/id is
entered) to improve UX; finally delete the `@ts-expect-error` line so TypeScript
error is resolved by correct typing.
packages/api/src/routers/forms.ts (1)

424-427: Extract duplicated Zod error formatting into one helper.

Both branches now build nearly identical issue messages. Consolidating this avoids message drift and keeps API validation output consistent.

♻️ Suggested refactor
+const formatZodIssues = (issues: z.ZodIssue[], prefix: string) => {
+  const errorMessages = issues.map((err) => {
+    const path = err.path.join(".");
+    return path ? `${path}: ${err.message}` : err.message;
+  });
+  return errorMessages.length > 0
+    ? `${prefix}: ${errorMessages.join("; ")}`
+    : prefix;
+};
...
-const errorMessages = response.error.issues.map((err) => {
-  const path = err.path.join(".");
-  return path ? `${path}: ${err.message}` : err.message;
-});
-const errorMessage =
-  errorMessages.length > 0
-    ? `Form response failed form validation: ${errorMessages.join("; ")}`
-    : "Form response failed form validation";
+const errorMessage = formatZodIssues(
+  response.error.issues,
+  "Form response failed form validation",
+);
...
-const errorMessages = validationResult.error.issues.map((err) => {
-  const path = err.path.join(".");
-  return path ? `${path}: ${err.message}` : err.message;
-});
+const errorMessage = formatZodIssues(
+  validationResult.error.issues,
+  "Form response failed validation",
+);
 throw new TRPCError({
-  message: `Form response failed validation: ${errorMessages.join("; ")}`,
+  message: errorMessage,
   code: "BAD_REQUEST",
 });

Also applies to: 505-508

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/routers/forms.ts` around lines 424 - 427, The Zod error
formatting logic is duplicated where response.error.issues.map builds
path-prefixed messages (seen in the errorMessages assignment and again around
lines 505-508); extract that logic into a single helper (e.g., formatZodError or
formatZodErrors) and replace both usages with calls to that helper so both
branches produce identical messages; the helper should accept an array of Zod
issues or a single issue and return the same `${path ? `${path}: ` :
""}${err.message}` output used currently by the errorMessages mapping, then
import/declare and use that helper wherever response.error.issues.map was used.
apps/2025/src/app/_components/partners/partnerPosters.tsx (1)

115-120: Add explicit link label for accessibility consistency.

The image alt gives an accessible name today, but an explicit aria-label on the link keeps semantics stable if internals change later.

♿ Suggested tweak
               <Link
                 href={partner.link}
+                aria-label={`${partner.alt} partner link`}
                 target="_blank"
                 rel="noopener noreferrer"
                 className="group relative flex h-full items-center justify-center rounded-none focus:outline-4 focus:outline-offset-2 focus:outline-[`#FBB03B`]"
               >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/2025/src/app/_components/partners/partnerPosters.tsx` around lines 115 -
120, Add an explicit accessible name to the partner link by adding an aria-label
to the Link component that uses the partner's identifying text (e.g.,
partner.name or partner.title) so the link's purpose remains clear even if the
internal image alt changes; update the Link element (the one rendering
href={partner.link}) to include aria-label={`Visit ${partner.name}`} (or
similar) ensuring it matches the image alt semantics.
apps/2025/src/app/_components/sponsors/sponsorPosters.tsx (1)

263-295: Avoid using alt text as rendering logic keys.

Line 263 and Line 287 couple behavior to display copy. If alt text changes, layout logic can break unexpectedly. Use explicit config fields instead.

🧩 Suggested refactor
 interface Sponsor {
   src: string;
   alt: string;
   link: string;
   category: Tier;
+  renderVariant?: "default" | "googleDual";
+  imageClassName?: string;
   // Grid positioning for sm+ screens (6-column layout)
   gridPosition: string;
   // Grid positioning for mobile (4-column layout)
   mobilePosition: string;
   ariaLabel: string;
 }

- {sponsor.alt === "GOOGLE" ? (
+ {sponsor.renderVariant === "googleDual" ? (
    ...
  ) : (
    <Image
      src={sponsor.src}
      alt={sponsor.alt}
      fill
-     className={`object-contain drop-shadow-sm ${
-       sponsor.alt === "NVIDIA"
-         ? "p-2 sm:p-4 md:scale-125 md:p-6"
-         : "p-4 md:p-8"
-     } ${sponsor.alt === "GITHUB" ? "p-2 sm:p-4 md:scale-125 md:p-6" : ""} ${
-       sponsor.alt === "SHINIES PROPS" ? "brightness-0" : ""
-     }`}
+     className={`object-contain drop-shadow-sm ${sponsor.imageClassName ?? "p-4 md:p-8"}`}
      sizes="(max-width: 640px) 45vw, (max-width: 1024px) 16vw, 12vw"
      draggable={false}
    />
  )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/2025/src/app/_components/sponsors/sponsorPosters.tsx` around lines 263 -
295, The rendering logic in sponsorPosters relies on sponsor.alt (e.g., checks
like sponsor.alt === "GOOGLE", "NVIDIA", "GITHUB", "SHINIES PROPS") which
couples display/layout to copy; add an explicit config field on the sponsor
objects (e.g., sponsor.variant or sponsor.key) and update the JSX in
sponsorPosters to switch only on that field for selecting images and classes,
replacing all sponsor.alt comparisons (including the GOOGLE branch that picks
google-mobile.svg vs google.svg and the conditional className adjustments for
NVIDIA/GITHUB/SHINIES PROPS) so alt remains solely the Image alt text and layout
logic uses the new variant/key.
apps/blade/src/app/_components/settings/member-profile-form.tsx (2)

252-252: Default value type inconsistency with schema.

Same issue as member-application-form.tsx: the schema expects a string for gradYear (before transform), but the default is a number.

♻️ Suggested fix
-      gradYear: Number(initTermYear.year),
+      gradYear: initTermYear.year, // already a string from line 98

Note: initTermYear.year is already a string (see line 98: year: String(d.getUTCFullYear())), so the Number() conversion here actually introduces the mismatch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/settings/member-profile-form.tsx` at line 252,
The default for gradYear in the member-profile-form should be a string to match
the schema: change the assignment in the default object (the gradYear property
set from initTermYear) to use the existing string value from initTermYear.year
instead of wrapping it with Number(), i.e., remove the Number(...) conversion so
gradYear === initTermYear.year; this mirrors the fix applied in
member-application-form and keeps the pre-transform type consistent with the
schema.

117-117: TypeScript escape hatch — same pattern as member-application-form.

The suppression is well-documented. See the earlier comment in member-application-form.tsx for context on potential long-term improvements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/settings/member-profile-form.tsx` at line 117,
Keep the TypeScript suppression on the line with "// `@ts-expect-error` -- schema
uses .transform() so input≠output; ZodType<TIn,TIn> in useForm can't represent
this" as the same escape hatch used in member-application-form.tsx; update or
copy the longer explanatory comment from member-application-form.tsx near the
suppression to document why useForm + Zod transform requires the ts-expect-error
and mention this as a TODO for future refactor (reference symbols: useForm,
ZodType, .transform()).
apps/blade/src/app/_components/dashboard/member/member-application-form.tsx (2)

93-93: TypeScript escape hatch noted — consider documenting or revisiting.

The @ts-expect-error comment is well-explained: Zod transforms create schemas where input ≠ output types, and useForm's typing can't represent this directly. This is a known limitation when using .transform() with strongly-typed form libraries.

For future maintainability, you might explore creating a custom type helper or checking if @forge/ui/form (or your version of react-hook-form) supports separate input/output generics. That said, this suppression is reasonable given the constraint.

As per coding guidelines: **/*.{ts,tsx} — Flag @ts-expect-error usage and suggest proper typing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/dashboard/member/member-application-form.tsx`
at line 93, The `@ts-expect-error` on the useForm call (due to Zod schema
.transform() producing input≠output) should not be left undocumented: either
replace the suppression with a small, explicit typed workaround (e.g., a custom
helper type/cast that expresses the form input type) or add a clear inline TODO
comment referencing the exact limitation and the affected symbol (useForm and
the Zod schema variable in member-application-form.tsx), and add a brief note
about revisiting when react-hook-form / `@forge/ui/form` gains separate
input/output generics; ensure the change removes the generic `@ts-expect-error`
flag or documents why it remains and adds a lint-exempt comment if required by
project policy.

259-259: Default value type mismatch with schema input type.

The schema defines gradYear as:

gradYear: z.string().regex(/^\d{4}$/).transform(Number)

This expects a string input (for regex validation) that transforms to a number. Setting the default as a number (new Date().getFullYear() + 1) works at runtime because HTML inputs coerce values, but it's semantically inconsistent.

♻️ Suggested fix for type consistency
-      gradYear: new Date().getFullYear() + 1,
+      gradYear: String(new Date().getFullYear() + 1),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/dashboard/member/member-application-form.tsx`
at line 259, The default for gradYear in the form is a number but the schema
(gradYear: z.string().regex(/^\d{4}$/).transform(Number)) expects a string
input; change the default to a four-digit string (e.g., String(new
Date().getFullYear() + 1) or .toString()) where gradYear is set in the member
application form component so the initial/default value matches the schema's
input type and regex validation.
apps/club/src/app/_components/links/button.tsx (1)

14-59: Reduce duplicated JSX between button and anchor branches.

The two branches duplicate class strings and inner content, which makes style updates easy to miss in one branch.

Refactor sketch
 export default function Button({ icon: Icon, href, ...props }: Props) {
   const innerText = props.children;
+  const baseClassName =
+    "font-pragati transition-colors bg-linear-to-br group relative inline-flex transform items-center gap-2 rounded-[200px] border border-[`#D8B4FE`] bg-transparent px-14 py-3 text-[12px] font-normal leading-normal tracking-[1px] text-white duration-100 hover:bg-[`#D8B4FE`] hover:text-[`#0F172A`] md:px-24 md:text-[13px] md:font-bold md:tracking-[.8px]";
+
+  const content = (
+    <>
+      {Icon && (
+        <Icon
+          className="**:transition-colors **:duration-100 group-hover:**:fill-[`#0F172A`] absolute left-6 h-4 w-4 fill-current md:left-14 md:h-5 md:w-5"
+          aria-hidden="true"
+        />
+      )}
+      {innerText}
+      <MenuHorizontalSVG
+        className="md:right-15 **:transition-colors **:duration-100 **:stroke-[`#D7B4FE`] group-hover:**:stroke-[`#0F172A`] absolute right-6 h-4 w-4 md:h-5 md:w-5"
+        aria-hidden="true"
+      />
+    </>
+  );

   if (!href) {
-    return (/* duplicated button JSX */);
+    return (
+      <button className={baseClassName} {...props}>
+        {content}
+      </button>
+    );
   } else {
-    return (/* duplicated anchor JSX */);
+    return (
+      <a href={href} className={baseClassName} {...props}>
+        {content}
+      </a>
+    );
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/club/src/app/_components/links/button.tsx` around lines 14 - 59, The
button and anchor branches duplicate the same className and inner content;
refactor by extracting the shared class string (used on both the <button> and
<a>), the shared children (Icon, innerText, MenuHorizontalSVG), and rendering a
single element determined by href (e.g., const Tag = href ? 'a' : 'button') so
you only set href when present and spread {...props} on Tag; update usages of
Icon and MenuHorizontalSVG and keep aria-hidden props intact while removing the
duplicated JSX blocks.
apps/blade/src/app/_components/admin/club/data/member-data/ShirtSizePie.tsx (1)

154-155: Refactor Pie components to use the shape callback instead of activeIndex for Recharts v3 compatibility.

Recharts v3 removed the activeIndex prop entirely (not just from types). While the current code works at runtime with @ts-expect-error, this suppression violates TypeScript guidelines and masks deprecated behavior.

The proper migration uses Recharts' shape callback with React state—supported in v3.7.0:

Example refactor pattern
// Before: activeIndex with `@ts-expect-error`
const [activeSize, setActiveSize] = useState(shirtSizeData[0]?.name ?? null);
const activeIndex = useMemo(
  () => shirtSizeData.findIndex((item) => item.name === activeSize),
  [activeSize, shirtSizeData],
);

<Pie
  data={shirtSizeData}
  // `@ts-expect-error` -- recharts v3 removed activeIndex from Pie TS types
  activeIndex={activeIndex}
  activeShape={...}
/>

// After: shape callback (type-safe)
<Pie
  data={shirtSizeData}
  shape={(props) => {
    const isActive = shirtSizeData[props.index]?.name === activeSize;
    return (
      <Sector
        {...props}
        outerRadius={isActive ? (props.outerRadius ?? 0) + 10 : props.outerRadius}
      />
    );
  }}
  onClick={(_, index) => setActiveSize(shirtSizeData[index]?.name ?? null)}
/>

This pattern appears in 8 Pie components across the codebase. Consolidate the refactor into a shared hook or wrapper component to reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/admin/club/data/member-data/ShirtSizePie.tsx`
around lines 154 - 155, Replace the deprecated activeIndex usage in ShirtSizePie
(and the other 7 Pie components) with a type-safe shape callback and click
handler: remove the `@ts-expect-error` and activeIndex prop, keep the existing
active state (e.g., activeSize and setActiveSize), add onClick to setActiveSize
using the clicked index, and implement shape={(props) => { const isActive =
shirtSizeData[props.index]?.name === activeSize; return <Sector ...
outerRadius={isActive ? (props.outerRadius ?? 0) + 10 : props.outerRadius} /> }}
(or equivalent JSX) to render the active slice; consolidate duplication by
extracting this pattern into a shared hook/wrapper (e.g., useActivePie or
PieWithActiveShape) so all Pie components reuse the same shape callback, onClick
logic, and active-state comparison.
apps/blade/src/app/_components/admin/club/data/member-data/YearOfStudyPie.tsx (1)

216-217: Migrate from activeIndex to modern Recharts v3 shape prop pattern.

The @ts-expect-error suppresses a real type error—activeIndex was removed in Recharts v3.0. However, since the repo uses v3.7.0 (well beyond v3.5.0+), the proper solution is now available: use the shape prop with the isActive flag instead.

The shape prop in modern v3 provides equivalent per-slice customization without needing the escape hatch. Migrate the activeIndex and activeShape pattern to v3's native approach to eliminate the error and follow current library conventions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/blade/src/app/_components/admin/club/data/member-data/YearOfStudyPie.tsx`
around lines 216 - 217, The Pie uses the removed activeIndex prop; remove the
ts-expect-error and activeIndex usage and migrate to Recharts v3's shape prop
pattern: create/rename the existing renderActiveShape (or custom slice renderer)
into a component/function that accepts Recharts slice props and uses the
provided isActive boolean to apply the active styling, then pass that component
to the Pie via shape={CustomSlice} (or shape={(props) => <CustomSlice {...props}
/>}) and stop using activeShape/activeIndex; update any local state references
(e.g., activeIndex) to instead determine isActive inside your CustomSlice by
comparing index if you still need index-based logic, and remove the
`@ts-expect-error` line so TypeScript validates the new shape usage in
YearOfStudyPie.
apps/club/src/app/_components/landing/about.tsx (1)

16-16: Same pattern: gsap.registerPlugin at component level.

Similar to impact.tsx, consider moving the registration to module level for consistency and minor optimization. This is a recurring pattern across landing components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/club/src/app/_components/landing/about.tsx` at line 16,
gsap.registerPlugin(ScrollTrigger) is being called inside the component; move
that plugin registration to module-level so it runs once when the module loads
rather than on each render. Remove the gsap.registerPlugin(ScrollTrigger) call
from inside the component (where you currently call it) and add a single call to
gsap.registerPlugin(ScrollTrigger) at the top of the module (above the component
definition/imports) so the plugin is registered once for the module.
apps/club/src/app/_components/landing/impact.tsx (1)

12-13: Consider moving gsap.registerPlugin outside the component.

Calling gsap.registerPlugin(useGSAP, ScrollTrigger) inside the component body means it runs on every render. While GSAP handles duplicate registrations gracefully, it's more idiomatic to register plugins once at the module level:

+gsap.registerPlugin(ScrollTrigger);
+
 export default function Impact() {
-  gsap.registerPlugin(useGSAP, ScrollTrigger);

This is a minor optimization—GSAP won't break, but module-level registration is cleaner.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/club/src/app/_components/landing/impact.tsx` around lines 12 - 13, Move
the gsap.registerPlugin call out of the Impact React component and into module
scope so it runs once at import time instead of on every render; locate the
Impact function and remove the line calling gsap.registerPlugin(useGSAP,
ScrollTrigger) from inside it, then add a single module-level registration
statement that calls gsap.registerPlugin(useGSAP, ScrollTrigger) near the top of
the file (before the Impact export) so the plugin registration happens once.
apps/club/src/app/_components/landing/discover.tsx (1)

106-108: Update CoolButton2 to use Tailwind v4 gradient syntax for consistency.

The component currently uses bg-gradient-to-r from-purple-800 to-purple-900 (Tailwind v3 syntax), but the rest of the codebase has already migrated to bg-linear-to-r (v4 syntax). Update the class to match:

Suggested change
- className={`group relative z-10 overflow-hidden rounded-full bg-gradient-to-r from-purple-800 to-purple-900 px-6 py-3 font-semibold text-white ...`}
+ className={`group relative z-10 overflow-hidden rounded-full bg-linear-to-r from-purple-800 to-purple-900 px-6 py-3 font-semibold text-white ...`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/club/src/app/_components/landing/discover.tsx` around lines 106 - 108,
Update the Tailwind gradient syntax used on the CoolButton2 instance: locate the
CoolButton2 component usage (prop className on CoolButton2) and replace the old
Tailwind v3 gradient class (bg-gradient-to-r from-purple-800 to-purple-900) with
the v4 equivalent (bg-linear-to-r while keeping the same from- and to- color
classes) so the className uses bg-linear-to-r along with from-purple-800 and
to-purple-900 for consistency with the codebase.
apps/tk/package.json (1)

25-25: Verify cron typings after the node-cron v4 upgrade.

node-cron was bumped on Line 25, but @types/node-cron is still v3 on Line 35. That can leave stale typings against v4 APIs; consider removing @types/node-cron (if v4 ships its own types) or aligning versions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tk/package.json` at line 25, package.json lists "node-cron": "^4.2.1"
but still has "@types/node-cron" at v3; verify and fix the mismatch by checking
whether node-cron v4 includes its own TypeScript declarations—if it does, remove
the "@types/node-cron" dependency; otherwise update or align "@types/node-cron"
to a version compatible with node-cron v4; update package.json accordingly and
run a type check to ensure no stale typings remain (references: dependency names
"node-cron" and "@types/node-cron").
packages/api/src/routers/dues-payment.ts (1)

80-81: Reuse the shared Stripe client instead of re-instantiating it.

Line 80 shadows the imported stripe client and creates a new instance per request. Reusing the shared client from Line 13 keeps config centralized.

Proposed fix
-      const stripe = new Stripe(env.STRIPE_SECRET_KEY);
-      const session = await stripe.checkout.sessions.retrieve(input);
+      const session = await stripe.checkout.sessions.retrieve(input);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/routers/dues-payment.ts` around lines 80 - 81, Remove the
per-request Stripe instantiation that shadows the module-scoped client: delete
the line "const stripe = new Stripe(env.STRIPE_SECRET_KEY);" and call the
existing imported/shared stripe client (the module-level "stripe" variable
referenced earlier) when invoking stripe.checkout.sessions.retrieve(input),
ensuring you don't re-declare or shadow the "stripe" identifier in the
dues-payment handler.
packages/ui/src/chart.tsx (1)

108-133: Avoid hand-maintaining Recharts payload prop shapes.

The custom tooltip/legend payload interfaces can drift from upstream Recharts types over time. Prefer deriving these from RechartsPrimitive.Tooltip/RechartsPrimitive.Legend prop types to keep wrappers upgrade-safe.

Also applies to: 284-294

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/chart.tsx` around lines 108 - 133, Replace the
hand-maintained tooltip/legend prop shapes with types derived from Recharts'
official prop types: import the appropriate Recharts primitive prop types (e.g.,
TooltipProps and LegendProps or RechartsPrimitive.Tooltip /
RechartsPrimitive.Legend) and use those (or their payload/member types like
TooltipProps['payload'] and LegendProps['payload']) instead of the inline
React.ComponentProps<"div"> & { ... } definition found in the custom
tooltip/legend prop typings (the inline union around active, payload, label,
formatter, color, hideLabel, hideIndicator, indicator, nameKey, labelKey) and
make the same replacement at the other occurrence around lines 284-294 so your
wrappers derive their payload shapes from upstream Recharts types and stay
upgrade-safe.
apps/2025/package.json (1)

18-34: Dependency upgrades look well-coordinated.

The major version bumps (Next.js 16, React 19, tRPC 11.10, TanStack Query 5.90) align with the PR objectives. One minor note:

Line 32: superjson is pinned exactly (2.2.6) while other dependencies use caret ranges (^). Consider using ^2.2.6 for consistency unless there's a specific reason for the exact pin.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/2025/package.json` around lines 18 - 34, Update the exact-pinned
dependency for superjson to use a caret range for consistency: change the
"superjson" entry in package.json (the dependency key "superjson" currently set
to "2.2.6") to "^2.2.6" so it matches the versioning style used by the other
dependencies.
pnpm-workspace.yaml (1)

17-22: Catalog name react18 is now misleading.

The react18 catalog now contains React 19 versions. Consider renaming to react19 or just react to avoid confusion for future maintainers.

♻️ Suggested rename
 catalogs:
-  react18:
+  react19:
     react: 19.2.4
     react-dom: 19.2.4
     "@types/react": ^19.2.14
     "@types/react-dom": ^19.2.3

Note: This would require updating references across all package.json files using catalog:react18.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pnpm-workspace.yaml` around lines 17 - 22, Rename the misleading catalog key
"react18" to a name that reflects the actual versions (e.g., "react19" or
"react") in pnpm-workspace.yaml (the catalogs: react18 block) and update all
package.json entries that reference "catalog:react18" to the new catalog name;
ensure you search the repo for the exact string "catalog:react18" and replace it
consistently so package resolution remains correct.
apps/tk/src/commands/cat.ts (1)

53-59: Consider adding user feedback on error (consistent with other commands).

Same recommendation as the other animal commands—reply to the interaction so users know something went wrong.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tk/src/commands/cat.ts` around lines 53 - 59, The catch block in the
command handler currently only logs errors to console; update the error handling
in the catch (err: unknown) of the cat command so it also sends a user-facing
reply via the interaction (e.g., interaction.reply or interaction.followUp)
consistent with other animal commands, include a brief friendly message like
"Something went wrong while fetching the cat" and, if err is an Error,
optionally log err.message to the console as before while ensuring the
interaction reply is always sent to the user to avoid leaving them without
feedback.
apps/tk/src/commands/duck.ts (1)

45-51: Consider replying to the user on failure.

When an error occurs, the interaction is never replied to—Discord will show "Application did not respond" after 3 seconds. Per coding guidelines, Discord bot commands should provide user-friendly error messages.

🛠️ Suggested fix
   } catch (err: unknown) {
     if (err instanceof Error) {
       console.error(err.message);
     } else {
       console.error("An unknown error occurred: ", err);
     }
+    void interaction.reply({
+      content: "🦆 Quack! Something went wrong fetching a duck image.",
+      ephemeral: true,
+    });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tk/src/commands/duck.ts` around lines 45 - 51, The catch block in the
duck command currently only logs errors to console and never replies to Discord,
causing "Application did not respond"; update the catch in the command handler
(the catch for the duck command's execution) to send a user-friendly reply to
the interaction (use interaction.reply or interaction.followUp) indicating an
error occurred, and include a brief message like "Something went wrong — please
try again later"; also ensure you handle already-replied interactions (check
interaction.replied or use deferred reply) and still log the full error for
diagnostics (include the caught err when calling your logger).
apps/tk/src/commands/fox.ts (1)

42-48: Same error handling gap as other animal commands.

Consider adding a user-facing reply on failure to avoid the "Application did not respond" timeout. This applies to all similar commands (cat, dog, capybara, duck, fox).

🛠️ Suggested fix
   } catch (err: unknown) {
     if (err instanceof Error) {
       console.error(err.message);
     } else {
       console.error("An unknown error occurred: ", err);
     }
+    void interaction.reply({
+      content: "🦊 Ring-ding-ding! Something went wrong.",
+      ephemeral: true,
+    });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tk/src/commands/fox.ts` around lines 42 - 48, The catch block in the fox
command's execute handler currently only logs errors (the console.error lines)
and never sends a user-facing reply, causing "Application did not respond"
timeouts; update the catch to both log the error and send a safe reply to the
user (e.g., via interaction.reply or interaction.followUp depending on whether a
reply was already sent) indicating the command failed, and ensure you guard with
interaction.replied/interaction.deferred checks so you don't attempt a second
reply; apply the same pattern used in the other animal command handlers (cat,
dog, capybara, duck) to keep behavior consistent.

ℹ️ Review info

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9cfa88 and 13abad8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (130)
  • .vscode/settings.json
  • apps/2025/next.config.js
  • apps/2025/package.json
  • apps/2025/src/app/_components/navbar/FloatingNav.tsx
  • apps/2025/src/app/_components/partners/partnerPosters.tsx
  • apps/2025/src/app/_components/sponsors/sponsorPosters.tsx
  • apps/2025/src/app/layout.tsx
  • apps/blade/next.config.js
  • apps/blade/package.json
  • apps/blade/postcss.config.cjs
  • apps/blade/src/app/_components/admin/charts/FirstTimeHackersPie.tsx
  • apps/blade/src/app/_components/admin/charts/GenderPie.tsx
  • apps/blade/src/app/_components/admin/charts/RaceOrEthnicityPie.tsx
  • apps/blade/src/app/_components/admin/charts/SchoolYearPie.tsx
  • apps/blade/src/app/_components/admin/club/data/event-data/AttendancesBarChart.tsx
  • apps/blade/src/app/_components/admin/club/data/event-data/HowFound.tsx
  • apps/blade/src/app/_components/admin/club/data/event-data/TypePie.tsx
  • apps/blade/src/app/_components/admin/club/data/member-data/GenderPie.tsx
  • apps/blade/src/app/_components/admin/club/data/member-data/ShirtSizePie.tsx
  • apps/blade/src/app/_components/admin/club/data/member-data/YearOfStudyPie.tsx
  • apps/blade/src/app/_components/admin/club/members/update-member.tsx
  • apps/blade/src/app/_components/admin/forms/responses/ResponseHorizontalBarChart.tsx
  • apps/blade/src/app/_components/admin/hackathon/data/LevelOfStudyPie.tsx
  • apps/blade/src/app/_components/admin/hackathon/data/ShirtSizePie.tsx
  • apps/blade/src/app/_components/dashboard/hacker/hacker-application-form.tsx
  • apps/blade/src/app/_components/dashboard/member/member-application-form.tsx
  • apps/blade/src/app/_components/forms/form-runner.tsx
  • apps/blade/src/app/_components/judge/results-table.tsx
  • apps/blade/src/app/_components/settings/hacker-profile-form.tsx
  • apps/blade/src/app/_components/settings/member-profile-form.tsx
  • apps/blade/src/app/admin/forms/[slug]/page.tsx
  • apps/blade/src/app/admin/forms/[slug]/responses/page.tsx
  • apps/blade/src/app/api/membership/route.ts
  • apps/blade/src/app/forms/[formName]/[responseId]/page.tsx
  • apps/blade/src/app/forms/[formName]/page.tsx
  • apps/blade/src/app/globals.css
  • apps/blade/src/app/hacker/application/[hackathon-id]/page.tsx
  • apps/blade/src/trpc/server.ts
  • apps/blade/tailwind.config.ts
  • apps/club/next.config.js
  • apps/club/package.json
  • apps/club/postcss.config.cjs
  • apps/club/src/app/_components/landing/about.tsx
  • apps/club/src/app/_components/landing/calendar.tsx
  • apps/club/src/app/_components/landing/discover.tsx
  • apps/club/src/app/_components/landing/hero.tsx
  • apps/club/src/app/_components/landing/impact-assets/wave-reveal.tsx
  • apps/club/src/app/_components/landing/impact.tsx
  • apps/club/src/app/_components/landing/sponsors-assets/sponsor-card.tsx
  • apps/club/src/app/_components/landing/sponsors-assets/sponsor-marquee.tsx
  • apps/club/src/app/_components/links/assets/abstract-shape-left-1.tsx
  • apps/club/src/app/_components/links/assets/abstract-shape-left-2.tsx
  • apps/club/src/app/_components/links/assets/abstract-shape-right-1.tsx
  • apps/club/src/app/_components/links/assets/abstract-shape-right-2.tsx
  • apps/club/src/app/_components/links/assets/binary-icon.tsx
  • apps/club/src/app/_components/links/assets/blank-calendar.tsx
  • apps/club/src/app/_components/links/assets/chat-bubble.tsx
  • apps/club/src/app/_components/links/assets/facebook.tsx
  • apps/club/src/app/_components/links/assets/instagram.tsx
  • apps/club/src/app/_components/links/assets/kh-logo.tsx
  • apps/club/src/app/_components/links/assets/knighthacks-text.tsx
  • apps/club/src/app/_components/links/assets/laptop-charging.tsx
  • apps/club/src/app/_components/links/assets/linkedin.tsx
  • apps/club/src/app/_components/links/assets/menu-horizontal.tsx
  • apps/club/src/app/_components/links/assets/outline.tsx
  • apps/club/src/app/_components/links/assets/terminal-icon.tsx
  • apps/club/src/app/_components/links/assets/tk-neon-sign.tsx
  • apps/club/src/app/_components/links/assets/tk-neon.tsx
  • apps/club/src/app/_components/links/assets/twitter.tsx
  • apps/club/src/app/_components/links/assets/youtube.tsx
  • apps/club/src/app/_components/links/button.tsx
  • apps/club/src/app/_components/links/links-header.tsx
  • apps/club/src/app/globals.css
  • apps/club/tailwind.config.ts
  • apps/cron/package.json
  • apps/gemiknights/next.config.js
  • apps/gemiknights/package.json
  • apps/gemiknights/postcss.config.cjs
  • apps/gemiknights/src/app/_components/graphics/gemini.tsx
  • apps/gemiknights/src/app/_components/graphics/mlhW.tsx
  • apps/gemiknights/src/app/_components/navbar/Navbar.tsx
  • apps/gemiknights/src/app/_components/ui/background-gradient-animation.tsx
  • apps/gemiknights/src/app/globals.css
  • apps/gemiknights/tailwind.config.ts
  • apps/guild/next.config.js
  • apps/guild/package.json
  • apps/guild/postcss.config.cjs
  • apps/guild/src/app/_components/guild-member-display.tsx
  • apps/guild/src/app/globals.css
  • apps/guild/src/app/page.tsx
  • apps/guild/src/trpc/server.ts
  • apps/guild/tailwind.config.ts
  • apps/tk/package.json
  • apps/tk/src/commands/capybara.ts
  • apps/tk/src/commands/cat.ts
  • apps/tk/src/commands/dog.ts
  • apps/tk/src/commands/duck.ts
  • apps/tk/src/commands/fox.ts
  • package.json
  • packages/api/package.json
  • packages/api/src/routers/dues-payment.ts
  • packages/api/src/routers/email.ts
  • packages/api/src/routers/forms.ts
  • packages/api/src/routers/judge.ts
  • packages/api/src/utils.ts
  • packages/auth/package.json
  • packages/auth/src/config.ts
  • packages/auth/src/index.rsc.ts
  • packages/consts/package.json
  • packages/db/package.json
  • packages/email/package.json
  • packages/ui/package.json
  • packages/ui/src/calendar.tsx
  • packages/ui/src/chart.tsx
  • packages/ui/src/dialog.tsx
  • packages/ui/src/dropdown-menu.tsx
  • packages/ui/src/form.tsx
  • packages/ui/src/navigation-menu.tsx
  • packages/ui/src/popover.tsx
  • packages/ui/src/select.tsx
  • packages/ui/src/sheet.tsx
  • packages/ui/src/tooltip.tsx
  • packages/validators/package.json
  • pnpm-workspace.yaml
  • tooling/eslint/package.json
  • tooling/prettier/package.json
  • tooling/tailwind/base.ts
  • tooling/tailwind/package.json
  • tooling/tailwind/web.ts
  • turbo.json
💤 Files with no reviewable changes (9)
  • apps/blade/src/app/_components/admin/club/data/event-data/AttendancesBarChart.tsx
  • tooling/tailwind/web.ts
  • apps/2025/src/app/layout.tsx
  • apps/2025/next.config.js
  • apps/gemiknights/next.config.js
  • apps/guild/next.config.js
  • apps/club/next.config.js
  • apps/blade/next.config.js
  • apps/blade/src/app/_components/admin/forms/responses/ResponseHorizontalBarChart.tsx

Comment on lines +149 to 150
// @ts-expect-error -- recharts v3 removed activeIndex from Pie TS types
activeIndex={activeIndex}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, examine the file structure and locate GenderPie.tsx
fd "GenderPie.tsx" --type f

Repository: KnightHacks/forge

Length of output: 191


🏁 Script executed:

# Read the relevant section of GenderPie.tsx
if [ -f "apps/blade/src/app/_components/admin/charts/GenderPie.tsx" ]; then
  wc -l "apps/blade/src/app/_components/admin/charts/GenderPie.tsx"
fi

Repository: KnightHacks/forge

Length of output: 122


🏁 Script executed:

# Get the context around lines 140-160 to see the full picture
if [ -f "apps/blade/src/app/_components/admin/charts/GenderPie.tsx" ]; then
  cat -n "apps/blade/src/app/_components/admin/charts/GenderPie.tsx" | sed -n '140,160p'
fi

Repository: KnightHacks/forge

Length of output: 985


🌐 Web query:

Recharts v3 activeIndex prop Pie chart removed replacement shape render

💡 Result:

In Recharts v3, <Pie activeIndex=... /> was removed. The “active” slice is now driven by <Tooltip /> interaction state, not by a Pie prop. [1][4]

Replacement for activeIndex

Use Tooltip props to control what’s active:

  • defaultIndex = initial active slice
  • trigger="hover" | "click" = how a slice becomes active
  • active = keep tooltip (and thus “active” state) visible once activated
  • content={() => null} if you want highlighting without rendering a tooltip UI
    [1][3]
<PieChart>
  <Tooltip defaultIndex={0} trigger="hover" />
  <Pie data={data} dataKey="value" />
</PieChart>

Replacement for activeShape / “render active slice”

In v3, activeShape is deprecated. Use shape and branch on the injected isActive flag to render the active slice differently. [2][4]

import { Sector } from "recharts";

<Pie
  data={data}
  dataKey="value"
  shape={(props) => (
    <Sector
      {...props}
      outerRadius={props.isActive ? props.outerRadius + 8 : props.outerRadius}
    />
  )}
/>

If you need the slice index inside shape

Recharts added the missing Sector index to the Pie shape callback (so you can style a specific slice similarly to old activeIndex patterns). Make sure you’re on a version that includes that fix. [4]

Sources: [1] [2] [3] [4]


🏁 Script executed:

# Check how activeIndex and activeLevel are defined/used in GenderPie.tsx
cat -n "apps/blade/src/app/_components/admin/charts/GenderPie.tsx" | head -n 100

Repository: KnightHacks/forge

Length of output: 3525


🏁 Script executed:

# Search for activeIndex and activeLevel definitions
grep -n "activeIndex\|activeLevel" "apps/blade/src/app/_components/admin/charts/GenderPie.tsx"

Repository: KnightHacks/forge

Length of output: 610


Remove @ts-expect-error and migrate activeIndex/activeShape from Recharts v2 to v3 patterns.

Lines 149–150 violate the TypeScript guidelines by suppressing a real API change: Recharts v3 removed the activeIndex prop and deprecated activeShape. The current code is using v2 APIs that no longer exist.

Use the shape callback with conditional rendering based on the activeLevel state to highlight the selected gender slice:

Suggested fix
-              // `@ts-expect-error` -- recharts v3 removed activeIndex from Pie TS types
-              activeIndex={activeIndex}
-              activeShape={({
-                outerRadius = 0,
-                ...props
-              }: PieSectorDataItem) => (
-                <g>
-                  <Sector {...props} outerRadius={outerRadius + 10} />
-                  <Sector
-                    {...props}
-                    outerRadius={outerRadius + 25}
-                    innerRadius={outerRadius + 12}
-                  />
-                </g>
-              )}
+              shape={(props: PieSectorDataItem) => {
+                const { outerRadius = 0 } = props;
+                const payload = props.payload as { name?: string } | undefined;
+                const isSelected = payload?.name === activeLevel;
+
+                if (!isSelected) return <Sector {...props} />;
+
+                return (
+                  <g>
+                    <Sector {...props} outerRadius={outerRadius + 10} />
+                    <Sector
+                      {...props}
+                      outerRadius={outerRadius + 25}
+                      innerRadius={outerRadius + 12}
+                    />
+                  </g>
+                );
+              }}

In Recharts v3, shape receives each sector's data and allows you to conditionally render based on external state—here, comparing the sector's name to activeLevel to determine highlighting. This removes the type error, eliminates the deprecated API usage, and properly achieves the same visual effect.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/admin/charts/GenderPie.tsx` around lines 149 -
150, The component is using the removed Recharts v3 props activeIndex and
activeShape (suppressed with `@ts-expect-error`); remove the ts-expect-error and
the activeIndex prop on the Pie and instead supply a shape callback that
inspects each sector's data (e.g., sector.name) and compares it to the
component's activeLevel state to decide whether to render the highlighted
variant. Update the Pie in GenderPie to use shape={customShape} (or an inline
function) that receives the sector payload and returns the highlighted or normal
sector markup based on payload.name === activeLevel, and remove any references
to activeIndex/activeShape.

Comment on lines +138 to 139
// @ts-expect-error -- recharts v3 removed activeIndex from Pie TS types
activeIndex={activeIndex}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the file and check its structure
fd "HowFound.tsx" --type f

Repository: KnightHacks/forge

Length of output: 131


🏁 Script executed:

# Check Recharts version in the codebase
find . -name "package.json" -type f -exec grep -l "recharts" {} \; | head -5

Repository: KnightHacks/forge

Length of output: 113


🏁 Script executed:

# Once we locate the file, examine lines around 138-139
cat -n apps/blade/src/app/_components/admin/club/data/event-data/HowFound.tsx | sed -n '130,150p'

Repository: KnightHacks/forge

Length of output: 976


🌐 Web query:

Recharts v3 activeIndex prop removal Pie component migration guide

💡 Result:

What changed in Recharts v3

  • activeIndex was removed (including for Pie) because Recharts already manages “active” state internally via <Tooltip />, and having both led to unpredictable behavior. [1][2]

Recommended migration (Pie)

1) Delete activeIndex usage on <Pie />

Before (v2.x):

<Pie activeIndex={activeIndex} activeShape={renderActiveShape} ... />

After (v3.x): remove activeIndex, and let <Tooltip /> drive what’s “active”. [1][2]

2) Use <Tooltip /> to control the active slice

Key replacements:

  • defaultIndex: initial highlighted slice (replacement for “initial activeIndex”) [1]
  • active: keep tooltip/active state “locked on” (if you need persistent highlight) [1]
  • content={() => null} and/or cursor={false}: keep highlight behavior while hiding tooltip UI [1]

Example:

<PieChart>
  <Pie data={data} dataKey="value" /* no activeIndex */ />
  <Tooltip defaultIndex={0} />
</PieChart>

[1]

3) If you used activeShape to render an expanded slice

In v3, Pie is moving toward a unified shape API:

  • activeShape / inactiveShape are deprecated in favor of shape, using the isActive flag passed to the callback. [3]
  • Recent v3 releases also added missing info (like sector index) to make “active-slice” customization easier again. [4]

Pattern:

<Pie
  data={data}
  dataKey="value"
  shape={(props) => {
    return props.isActive ? /* render active sector */ : /* normal sector */;
  }}
/>

[3][4]

Sources: Recharts “Active Index” guide [1], Recharts v3 migration guide [2], Pie API docs (deprecation note) [3], Recharts release notes (Pie shape/index fixes) [4].


🏁 Script executed:

# Read the full file to understand activeLevel and component structure
wc -l apps/blade/src/app/_components/admin/club/data/event-data/HowFound.tsx

Repository: KnightHacks/forge

Length of output: 135


🏁 Script executed:

# Get full file content with line numbers
cat -n apps/blade/src/app/_components/admin/club/data/event-data/HowFound.tsx

Repository: KnightHacks/forge

Length of output: 7556


🏁 Script executed:

# Check imports and understand PieSectorDataItem type
head -50 apps/blade/src/app/_components/admin/club/data/event-data/HowFound.tsx

Repository: KnightHacks/forge

Length of output: 1441


🏁 Script executed:

# Search for activeLevel usage in the file
grep -n "activeLevel" apps/blade/src/app/_components/admin/club/data/event-data/HowFound.tsx

Repository: KnightHacks/forge

Length of output: 378


🌐 Web query:

Recharts v3 Pie shape prop callback signature what properties are passed

💡 Result:

In Recharts v3, Pie’s shape prop can be a ReactNode or a function with this signature:

shape?: ReactNode | ((props: PieSectorShapeProps, index: number) => ReactElement)

where:

type PieSectorShapeProps = PieSectorDataItem & { isActive: boolean; index: number };

[1]

What’s in props (PieSectorShapeProps)

It’s essentially all the computed sector geometry + your presentation props + data info, including (non-exhaustive but the main ones):

  • Geometry / positioning: cx, cy, innerRadius, outerRadius, startAngle, endAngle, cornerRadius, plus other sector-geometry fields coming from GeometrySector [1]
  • Data-related: value, name?, percent?, payload? (original data item), dataKey? [1]
  • Tooltip-related: tooltipPayload?, tooltipPosition [1]
  • Interaction state: isActive: boolean [1]
  • Slice index: index: number (also passed as the 2nd function argument) [1]

Note about index

Recharts added the sector index to the shape callback props in v3.5.1 (it was missing in v3.5.0). [2]

Sources: Pie type definitions [1], v3.5.1 release notes mentioning the added index [2].


🏁 Script executed:

# Search for other Pie chart usages in the codebase to see if there are examples of v3 patterns
rg "shape=\{" apps/blade --type tsx -A 5 -B 2

Repository: KnightHacks/forge

Length of output: 88


🏁 Script executed:

# Search for other Pie chart usages to see if there are v3 pattern examples
rg "shape=" apps/blade -A 5 -B 2 --type typescript

Repository: KnightHacks/forge

Length of output: 95


🏁 Script executed:

# Check Recharts version in package.json to confirm v3
cat apps/blade/package.json | grep -A 2 -B 2 recharts

Repository: KnightHacks/forge

Length of output: 205


🏁 Script executed:

# Check if there are other similar patterns or if this is the only place using activeIndex
rg "activeIndex" apps/blade -B 5 -A 5

Repository: KnightHacks/forge

Length of output: 39886


🏁 Script executed:

# Verify the full context of line 169 and ensure activeIndex is only used there
sed -n '160,180p' apps/blade/src/app/_components/admin/club/data/event-data/HowFound.tsx

Repository: KnightHacks/forge

Length of output: 893


Migrate to Recharts v3 shape prop instead of using activeIndex prop.

Line 138 suppresses a type error for a removed Recharts v3 prop. The activeIndex prop no longer exists in v3; use the shape callback instead. Keep the activeIndex computation since it's still used at line 169 for the label.

💡 Suggested fix
              data={foundData}
              dataKey="amount"
              nameKey="name"
              innerRadius={60}
              strokeWidth={5}
-             // `@ts-expect-error` -- recharts v3 removed activeIndex from Pie TS types
-             activeIndex={activeIndex}
-             activeShape={({
-               outerRadius = 0,
-               ...props
-             }: PieSectorDataItem) => (
-               <g>
-                 <Sector {...props} outerRadius={outerRadius + 10} />
-                 <Sector
-                   {...props}
-                   outerRadius={outerRadius + 25}
-                   innerRadius={outerRadius + 12}
-                 />
-               </g>
-             )}
+             shape={({ outerRadius = 0, index, ...props }: PieSectorDataItem & { index: number }) => {
+               const isSelected = index === activeIndex;
+               if (!isSelected) return <Sector {...props} />;
+               return (
+                 <g>
+                   <Sector {...props} outerRadius={outerRadius + 10} />
+                   <Sector
+                     {...props}
+                     outerRadius={outerRadius + 25}
+                     innerRadius={outerRadius + 12}
+                   />
+                 </g>
+               );
+             }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// @ts-expect-error -- recharts v3 removed activeIndex from Pie TS types
activeIndex={activeIndex}
data={foundData}
dataKey="amount"
nameKey="name"
innerRadius={60}
strokeWidth={5}
shape={({ outerRadius = 0, index, ...props }: PieSectorDataItem & { index: number }) => {
const isSelected = index === activeIndex;
if (!isSelected) return <Sector {...props} />;
return (
<g>
<Sector {...props} outerRadius={outerRadius + 10} />
<Sector
{...props}
outerRadius={outerRadius + 25}
innerRadius={outerRadius + 12}
/>
</g>
);
}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/admin/club/data/event-data/HowFound.tsx`
around lines 138 - 139, The Pie component is still passing the removed Recharts
v3 prop activeIndex (suppressed by ts-expect-error); remove that prop and
instead provide a shape={(sliceProps) => ...} callback that uses the existing
activeIndex computation to render the highlighted/active slice (call your
existing renderActiveShape or implement the highlight logic inside the shape
callback). Keep the activeIndex variable because it’s still used for the label
rendering, ensure the shape callback receives the correct slice props type
(adjust typings if needed), and delete the ts-expect-error line so the code
compiles cleanly with Recharts v3.

lastName: member.lastName || "",
email: member.email || "",
points: (member.points ?? 0).toString(),
points: member.points ?? 0,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Type mismatch: numeric default vs. string schema input.

The schema on line 75 expects z.string() input, but this default provides a number (member.points ?? 0). While HTML inputs coerce values to strings at runtime, this creates a TypeScript inconsistency that contributes to the @ts-expect-error above.

If you adopt the z.coerce.number() refactor suggested above, this line becomes type-correct as-is. Otherwise, for consistency with the current string-based schema:

-  points: member.points ?? 0,
+  points: String(member.points ?? 0),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
points: member.points ?? 0,
points: String(member.points ?? 0),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/admin/club/members/update-member.tsx` at line
88, The default value for points (points: member.points ?? 0) is a number but
the form schema uses z.string(), causing a TS mismatch; either change the schema
to accept a numeric input using z.coerce.number() (so points can remain numeric)
or keep the string schema and convert the default to a string (points:
String(member.points ?? 0)); update the matching form field and remove the
now-unnecessary `@ts-expect-error` accordingly (refer to the points property and
the z.string() schema in this component).

Comment on lines +192 to 193
// @ts-expect-error -- recharts v3 removed activeIndex from Pie TS types
activeIndex={activeIndex}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and read the relevant section
fd "LevelOfStudyPie.tsx" --type f

Repository: KnightHacks/forge

Length of output: 132


🏁 Script executed:

# Also search for similar patterns in the codebase
rg "@ts-expect-error|@ts-ignore" --type tsx --type ts apps/blade/ -A 2 -B 2 | head -100

Repository: KnightHacks/forge

Length of output: 88


🏁 Script executed:

# Look for any package.json to confirm Recharts version
fd "package.json" -x cat {} \; | grep -A 5 -B 5 "recharts" || true

Repository: KnightHacks/forge

Length of output: 657


🏁 Script executed:

# Read the LevelOfStudyPie.tsx file to see the actual code
cat -n apps/blade/src/app/_components/admin/hackathon/data/LevelOfStudyPie.tsx | head -210 | tail -50

Repository: KnightHacks/forge

Length of output: 2074


🏁 Script executed:

# Also check the full context around lines 192-193
cat -n apps/blade/src/app/_components/admin/hackathon/data/LevelOfStudyPie.tsx

Repository: KnightHacks/forge

Length of output: 9416


🌐 Web query:

Recharts v3 Pie component shape callback props signature activeIndex removed

💡 Result:

  • activeIndex was removed in Recharts v3.0 (for Pie/Bar/Scatter/etc.). The Recharts docs recommend controlling “what’s active” via <Tooltip /> interaction (e.g. defaultIndex, active, etc.) instead. [1], [2]

  • Pie “active slice” customization in v3 moved away from activeShape/inactiveShape (now deprecated) toward the shape prop, where you check props.isActive inside the callback. This shape approach for Pie was introduced in v3.5.0. [3], [4]

  • Pie shape callback signature (v3.7.x types):
    shape?: ReactNode | ((props: PieSectorShapeProps, index: number) => ReactElement)
    where PieSectorShapeProps includes the sector geometry/data plus:

    • isActive: boolean
    • index: number [4]
  • About the “index missing” / “activeIndex removed” confusion:
    When Pie.shape shipped in v3.5.0, the release notes note they “forgot to include the current Sector index”; it was added in v3.5.1 (“add missing Sector index to Pie shape prop”), enabling per-slice customization similar to what people used activeIndex for in v2.x. [3]

Sources:
[1] https://recharts.github.io/en-US/guide/activeIndex/
[2] https://github.com/recharts/recharts/wiki/3.0-migration-guide
[3] https://github.com/recharts/recharts/releases (v3.5.0 / v3.5.1 notes)
[4] https://www.jsdocs.io/package/recharts (v3.7.0 type aliases: PieShape, PieSectorShapeProps)


🏁 Script executed:

# Check if there are similar `@ts-expect-error` patterns in other pie/chart components
rg "@ts-expect-error" apps/blade --type ts --type tsx -B 2 -A 2

Repository: KnightHacks/forge

Length of output: 88


🏁 Script executed:

# Check for any other uses of activeIndex in recharts components
rg "activeIndex" apps/blade --type ts --type tsx -B 2 -A 2

Repository: KnightHacks/forge

Length of output: 88


🏁 Script executed:

# Search for `@ts-expect-error` in blade directory with simpler approach
rg "@ts-expect-error" apps/blade -B 2 -A 2

Repository: KnightHacks/forge

Length of output: 7960


🏁 Script executed:

# Search for activeIndex usage in blade directory
rg "activeIndex" apps/blade -B 2 -A 2

Repository: KnightHacks/forge

Length of output: 19250


Refactor pie charts off deprecated activeIndex API; affects 11 components.

Recharts v3 removed activeIndex from the Pie component types. This pattern is repeated across 11 pie chart files in the codebase and violates the TS escape-hatch guideline.

Migrate to the shape prop with isActive (available in Recharts v3.5+, current version 3.7.0). The suggested approach is sound, but the Label component also needs refactoring since it currently accesses levelData[activeIndex]—track the active item reference instead:

💡 Suggested approach

Replace activeIndex and activeShape with a shape renderer:

- // `@ts-expect-error` -- recharts v3 removed activeIndex from Pie TS types
- activeIndex={activeIndex}
- activeShape={({
-   outerRadius = 0,
-   ...props
- }: PieSectorDataItem) => (
-   <g>
-     <Sector {...props} outerRadius={outerRadius + 10} />
-     <Sector
-       {...props}
-       outerRadius={outerRadius + 25}
-       innerRadius={outerRadius + 12}
-     />
-   </g>
- )}
+ shape={(props: PieSectorDataItem) => {
+   const { outerRadius = 0, isActive } = props;
+   if (!isActive) return <Sector {...props} />;
+   return (
+     <g>
+       <Sector {...props} outerRadius={outerRadius + 10} />
+       <Sector
+         {...props}
+         outerRadius={outerRadius + 25}
+         innerRadius={outerRadius + 12}
+       />
+     </g>
+   );
+ }}

Then update the Label renderer to reference activeLevel directly instead of indexing with activeIndex:

- {levelData[activeIndex]?.amount.toLocaleString()}
+ {levelData.find(item => item.name === activeLevel)?.amount.toLocaleString()}

Apply this pattern across all 11 pie chart files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/admin/hackathon/data/LevelOfStudyPie.tsx`
around lines 192 - 193, Replace the deprecated Pie prop usage by removing the //
`@ts-expect-error` activeIndex={activeIndex} pattern and implement a custom shape
renderer for the Pie component that uses the renderer's isActive flag to set an
activeLevel reference; inside the shape renderer detect isActive and assign the
corresponding datum to activeLevel (instead of relying on activeIndex) and pass
that activeLevel into the Label renderer, then refactor the Label component
(currently accessing levelData[activeIndex]) to consume activeLevel directly;
apply the same replacement (use Pie's shape+isActive and Label activeLevel)
across all 11 pie chart components, keeping function/class names like Pie,
activeShape, Label, levelData, and activeIndex as the guide for locations to
change.

Comment on lines +154 to 155
// @ts-expect-error -- recharts v3 removed activeIndex from Pie TS types
activeIndex={activeIndex}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file at the specified lines
cat -n apps/blade/src/app/_components/admin/hackathon/data/ShirtSizePie.tsx | sed -n '145,165p'

Repository: KnightHacks/forge

Length of output: 988


🌐 Web query:

Recharts v3 Pie component activeIndex prop removal API changes

💡 Result:

In Recharts v3.0, the activeIndex prop was removed (including from <Pie />) because “active index” is already managed internally by <Tooltip />, and having both led to unpredictable behavior. Use Tooltip-driven interaction instead. [1], [2]

What to use instead of activeIndex (v3)

Use <Tooltip /> props to control what’s “active” / highlighted:

  • defaultIndex: set the initially-highlighted slice
  • active: keep tooltip/active state “stuck”
  • content={() => null}: keep interaction but hide the tooltip UI
  • (also relevant depending on chart) trigger, cursor [1]

Pie “active slice” rendering changed in v3.5.x

If you were using activeIndex mainly to render a custom active slice:

  • v3.5.0 added Pie shape (callback) and deprecated activeShape / inactiveShape; use the callback’s isActive to render an “active” sector. [3]
  • v3.5.1 added the missing sector index to the shape callback, explicitly noting it enables per-slice customization similar to activeIndex in 2.x. [4]

Sources: ActiveIndex guide [1], v3 migration guide [2], v3.5.0 release notes (shape + deprecations) [3], v3.5.1 release notes (index in shape callback) [4].


🏁 Script executed:

# Check Recharts version in the project
find . -name "package.json" -o -name "package-lock.json" | xargs grep -l "recharts" 2>/dev/null | head -3

Repository: KnightHacks/forge

Length of output: 113


🏁 Script executed:

# Check Recharts version in the project
cat apps/blade/package.json | grep -A 2 -B 2 "recharts"

Repository: KnightHacks/forge

Length of output: 205


🏁 Script executed:

# Also check if there are other uses of activeIndex or `@ts-expect-error` in Pie/chart components
rg "activeIndex|@ts-expect-error.*Pie" apps/blade/src/app/_components/admin/hackathon/data/ -A 2 -B 2

Repository: KnightHacks/forge

Length of output: 3591


🏁 Script executed:

# Check the full component to understand activeSize variable scope
cat -n apps/blade/src/app/_components/admin/hackathon/data/ShirtSizePie.tsx | head -50

Repository: KnightHacks/forge

Length of output: 1703


🏁 Script executed:

# Check the rest of ShirtSizePie.tsx to see how activeIndex is used elsewhere
cat -n apps/blade/src/app/_components/admin/hackathon/data/ShirtSizePie.tsx | sed -n '50,100p'

Repository: KnightHacks/forge

Length of output: 1914


🏁 Script executed:

# Check if LevelOfStudyPie has the same structure
cat -n apps/blade/src/app/_components/admin/hackathon/data/LevelOfStudyPie.tsx | head -70

Repository: KnightHacks/forge

Length of output: 2517


🏁 Script executed:

# Let's see how activeIndex is used in the label rendering
cat -n apps/blade/src/app/_components/admin/hackathon/data/ShirtSizePie.tsx | sed -n '100,180p'

Repository: KnightHacks/forge

Length of output: 3313


🏁 Script executed:

# Let's see the rest of the Label rendering
cat -n apps/blade/src/app/_components/admin/hackathon/data/ShirtSizePie.tsx | sed -n '170,210p'

Repository: KnightHacks/forge

Length of output: 1820


Remove the TypeScript suppression by refactoring to use Recharts v3.7.0's shape callback instead of the removed activeIndex prop.

Line 154 violates the no-escape-hatches guideline. Recharts v3 removed activeIndex from the Pie API, so using @ts-expect-error masks a real migration issue. Instead, keep activeIndex as a computed variable (needed for the Label's data access) but render the active sector visually using the shape callback with the isActive parameter (available since v3.5.0+).

Suggested refactor
              innerRadius={60}
              strokeWidth={5}
-             // `@ts-expect-error` -- recharts v3 removed activeIndex from Pie TS types
-             activeIndex={activeIndex}
              activeShape={({
                outerRadius = 0,
                ...props
-             }: PieSectorDataItem) => (
+             }: PieSectorDataItem & { isActive?: boolean }) => (
                <g>
-                 <Sector {...props} outerRadius={outerRadius + 10} />
-                 <Sector
-                   {...props}
-                   outerRadius={outerRadius + 25}
-                   innerRadius={outerRadius + 12}
-                 />
+                 {props.isActive && (
+                   <>
+                     <Sector {...props} outerRadius={outerRadius + 10} />
+                     <Sector
+                       {...props}
+                       outerRadius={outerRadius + 25}
+                       innerRadius={outerRadius + 12}
+                     />
+                   </>
+                 )}
                </g>
              )}

Keep the activeIndex variable as-is for the Label component (line 185)—it only needs the computed value for data access, not to pass to the Pie component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/admin/hackathon/data/ShirtSizePie.tsx` around
lines 154 - 155, The Pie usage in ShirtSizePie.tsx is silenced with a
ts-expect-error because Recharts v3 removed the activeIndex prop; remove the
ts-expect-error and the activeIndex prop on the <Pie> and instead pass a custom
shape function (shape={renderSector} or inline) that uses the isActive parameter
to draw the highlighted sector, while keeping the computed activeIndex variable
unchanged for the Label component’s data access; update the render function name
(e.g., renderActiveShape/renderSector) and ensure it accepts (props, isActive)
or the v3 signature so the active visual state is driven by isActive rather than
the removed prop.

{
<MenuHorizontalSVG
className="md:right-15 absolute right-6 h-4 w-4 fill-current md:h-5 md:w-5 [&_*]:transition-colors [&_*]:duration-100 group-hover:[&_*]:fill-[#0F172A]"
className="md:right-15 **:transition-colors **:duration-100 group-hover:**:fill-[#0F172A] absolute right-6 h-4 w-4 fill-current md:h-5 md:w-5"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd "button.tsx" | head -20

Repository: KnightHacks/forge

Length of output: 1503


🏁 Script executed:

# Search for MenuHorizontalSVG definition across the repo
rg "MenuHorizontalSVG" -A 5 -B 2

Repository: KnightHacks/forge

Length of output: 2679


🏁 Script executed:

cat apps/club/src/app/_components/links/assets/menu-horizontal.tsx

Repository: KnightHacks/forge

Length of output: 1437


🏁 Script executed:

cat -n apps/club/src/app/_components/links/button.tsx

Repository: KnightHacks/forge

Length of output: 2850


Update SVG styling to target stroke instead of fill.

The MenuHorizontalSVG paths are rendered with stroke attributes, not fill. The current group-hover:**:fill-[#0F172A] and fill-current utilities won't affect them. Change these to target stroke so the hover effect works as intended.

Proposed fix
- className="md:right-15 **:transition-colors **:duration-100 group-hover:**:fill-[`#0F172A`] absolute right-6 h-4 w-4 fill-current md:h-5 md:w-5"
+ className="md:right-15 **:transition-colors **:duration-100 **:stroke-[`#D7B4FE`] group-hover:**:stroke-[`#0F172A`] absolute right-6 h-4 w-4 md:h-5 md:w-5"

Apply to both lines 30 and 53.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/club/src/app/_components/links/button.tsx` at line 30, The hover color
utilities are targeting fill but the MenuHorizontalSVG uses stroke attributes;
update the className on the button/icon instances (the class containing
"group-hover:**:fill-[`#0F172A`]" and "fill-current") to use stroke utilities
instead—replace "group-hover:**:fill-[`#0F172A`]" with
"group-hover:**:stroke-[`#0F172A`]" and "fill-current" with "stroke-current";
apply the same replacements for both occurrences in button.tsx (the lines around
the MenuHorizontalSVG render).

Comment on lines +3 to +5
@import "tailwindcss";
@source "../../packages/ui/src";
@config "../../tailwind.config.ts";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find Stylelint configuration files
find . -type f \( -name ".stylelintrc*" -o -name "stylelint.config.*" -o -name ".stylelintignore" \) 2>/dev/null | head -20

# Also check package.json for stylelint config
find . -name "package.json" -type f -exec grep -l "stylelint" {} \; 2>/dev/null | head -5

Repository: KnightHacks/forge

Length of output: 80


🏁 Script executed:

#!/bin/bash
# Search for at-rule-no-unknown or ignore patterns in common config locations
rg -n "at-rule-no-unknown|ignoreAtRules|allowUnknown" --type json --type js --type yaml 2>/dev/null | head -30

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check the globals.css file to confirm the content
cat -n apps/club/src/app/globals.css 2>/dev/null || echo "File not found, searching for it..."
fd -H "globals.css" .

Repository: KnightHacks/forge

Length of output: 5145


🏁 Script executed:

cat -n ./.stylelintrc.json

Repository: KnightHacks/forge

Length of output: 1277


🏁 Script executed:

# Check if there are any other stylelint configurations in the monorepo
fd -H "\.stylelintrc" . 2>/dev/null

Repository: KnightHacks/forge

Length of output: 80


Update Stylelint config to allow Tailwind v4 at-rules.

The .stylelintrc.json extends stylelint-config-standard-scss, which has scss/at-rule-no-unknown enabled by default. Since @import "tailwindcss", @source, @config, @custom-variant, and @utility are Tailwind v4 directives (not standard SCSS), Stylelint will flag them as errors without explicit configuration.

Add this to .stylelintrc.json under "rules":

Stylelint config fix
"scss/at-rule-no-unknown": [true, {
  "ignoreAtRules": ["tailwind", "source", "config", "custom-variant", "utility"]
}]

This allows Stylelint to recognize Tailwind v4's custom at-rules and prevents CI build failures.

🧰 Tools
🪛 Stylelint (17.3.0)

[error] 4-4: Unexpected unknown at-rule "@source" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)


[error] 5-5: Unexpected unknown at-rule "@config" (scss/at-rule-no-unknown)

(scss/at-rule-no-unknown)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/club/src/app/globals.css` around lines 3 - 5, Update the Stylelint rules
to allow Tailwind v4 at-rules: in your .stylelintrc.json add/modify the
"scss/at-rule-no-unknown" rule to keep it enabled but ignore the
Tailwind-specific directives used in globals.css (referencing `@import`
"tailwindcss", `@source`, `@config`, `@custom-variant`, `@utility`). Specifically, set
"scss/at-rule-no-unknown" to an array form with true and an ignoreAtRules list
containing "tailwind", "source", "config", "custom-variant", and "utility" so
Stylelint no longer flags those at-rules during CI.

"node-cron": "^3.0.3",
"postgres": "^3.4.3",
"resend": "^3.2.0",
"node-cron": "^4.2.1",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

node-cron v4 TypeScript type definitions built-in

💡 Result:

node-cron v4 ships with built-in TypeScript type declarations, so you do not need @types/node-cron when using v4+. [1], [2]

If you previously had @types/node-cron installed (typically for v3 and earlier), remove it to avoid API/type mismatches with v4’s rewritten/changed TypeScript-based API. [2], [1]


🌐 Web query:

node-cron v4.2.1 types package.json

💡 Result:

For node-cron@4.2.1, TypeScript types are built in (no separate @types/node-cron package needed). The published package.json declares the .d.ts files via conditional exports:

  • main: ./dist/cjs/node-cron.js
  • module: ./dist/esm/node-cron.js
  • type: commonjs
  • exports["."]:
    • require.types: ./dist/cjs/node-cron.d.ts
    • import.types: ./dist/esm/node-cron.d.ts [1]

npm also indicates the package “contains built-in TypeScript declarations.” [2]

Sources: [1] [2]


Remove @types/node-cron — it's obsolete with node-cron v4.

node-cron v4.2.1 ships with built-in TypeScript type declarations (via conditional exports in its package.json). Keeping @types/node-cron v3 alongside it causes type conflicts and stale type signatures. Remove line 36 from package.json:

Expected change
- "@types/node-cron": "^3.0.11",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cron/package.json` at line 26, Remove the obsolete `@types/node-cron`
dependency from apps/cron package.json: locate the dependencies/devDependencies
entry for "@types/node-cron" (v3) and delete that line so only "node-cron":
"^4.2.1" remains, then run npm/yarn install to refresh lockfile and ensure
TypeScript uses the built-in types from node-cron v4.

Comment on lines +29 to +34
const img = await JIMP.read(data.data.url);
const { width, height } = img;
const color = img.getPixelColor(
Math.floor(width / 2),
Math.floor(height / 2),
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate API-provided image URLs and always reply on failure

Line 29 trusts data.data.url directly. If the API returns an invalid/unsupported URL, image parsing fails and users get no friendly response. Validate the URL (http/https only) before JIMP.read, and return an ephemeral fallback message in the error path.

🔧 Suggested fix
 export async function execute(interaction: CommandInteraction) {
   try {
     const res = await fetch(url);
     const data = (await res.json()) as CapybaraProps;
+    const imageUrl = data?.data?.url;
+    if (!imageUrl) {
+      throw new Error("Capybara API returned no image URL");
+    }
+
+    const parsedUrl = new URL(imageUrl);
+    if (!["http:", "https:"].includes(parsedUrl.protocol)) {
+      throw new Error("Unsupported image URL protocol");
+    }

     // get the average color of the img, make it the embed color
-    const img = await JIMP.read(data.data.url);
+    const img = await JIMP.read(parsedUrl.toString());
     const { width, height } = img;
     const color = img.getPixelColor(
       Math.floor(width / 2),
       Math.floor(height / 2),
     );
@@
     const embed = new EmbedBuilder()
-      .setImage(data.data.url)
+      .setImage(parsedUrl.toString())
       .setColor(`#${hexString}`);
     void interaction.reply({ embeds: [embed] });
     // catch any errors
   } catch (err: unknown) {
+    const message = "I couldn't fetch a capybara right now. Please try again in a moment.";
+    if (interaction.replied || interaction.deferred) {
+      await interaction.followUp({ content: message, ephemeral: true });
+    } else {
+      await interaction.reply({ content: message, ephemeral: true });
+    }
+
     if (err instanceof Error) {
       console.error(err.message);
     } else {
       console.error("An unknown error occurred: ", err);
     }
   }
 }

As per coding guidelines, "apps/tk/: Command handlers validate input" and "apps/tk/: Graceful error handling with user-friendly messages".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tk/src/commands/capybara.ts` around lines 29 - 34, Validate the
API-provided image URL before calling JIMP.read and return an ephemeral fallback
message on failure: check that data.data.url is a non-empty string with an http
or https scheme, then wrap the JIMP.read(...) call (which produces img and later
color via img.getPixelColor(...)) in a try/catch; on any validation failure or
exception, send the user a friendly ephemeral error reply and abort further
image processing, otherwise continue using img and color as before.

Comment on lines +24 to +29
const img = await JIMP.read(data.message);
const { width, height } = img;
const color = img.getPixelColor(
Math.floor(width / 2),
Math.floor(height / 2),
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate the API image URL before passing it to JIMP.read.

Line 24 trusts data.message directly from an external API. Add validation (shape + URL/protocol) before image decoding to prevent malformed payload crashes.

Proposed fix
-    const img = await JIMP.read(data.message);
+    if (typeof data.message !== "string") {
+      throw new Error("Dog API returned an invalid image URL");
+    }
+    const imageUrl = new URL(data.message);
+    if (imageUrl.protocol !== "https:") {
+      throw new Error("Dog API returned a non-HTTPS image URL");
+    }
+    const img = await JIMP.read(imageUrl.toString());

As per coding guidelines apps/tk/**: Command handlers validate input.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const img = await JIMP.read(data.message);
const { width, height } = img;
const color = img.getPixelColor(
Math.floor(width / 2),
Math.floor(height / 2),
);
if (typeof data.message !== "string") {
throw new Error("Dog API returned an invalid image URL");
}
const imageUrl = new URL(data.message);
if (imageUrl.protocol !== "https:") {
throw new Error("Dog API returned a non-HTTPS image URL");
}
const img = await JIMP.read(imageUrl.toString());
const { width, height } = img;
const color = img.getPixelColor(
Math.floor(width / 2),
Math.floor(height / 2),
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tk/src/commands/dog.ts` around lines 24 - 29, The handler currently
passes data.message directly into JIMP.read (see JIMP.read and subsequent use of
img.getPixelColor, width, height); add input validation before decoding: verify
data.message exists and is a string, parse it as a URL and ensure the protocol
is http or https, and optionally check it matches the expected host/shape or a
simple regex for image URLs (jpg/png/webp); if validation fails, return or throw
a clear error instead of calling JIMP.read so malformed payloads cannot crash
the image processing code.

coderabbitai[bot]
coderabbitai bot previously requested changes Feb 27, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/blade/src/app/_components/dashboard/member-dashboard/payment/payment-button.tsx (1)

36-41: ⚠️ Potential issue | 🟠 Major

Add error handling and loading state to the payment flow.

Per the coding guidelines, Stripe payment flows require thorough review. Currently:

  1. No error handling — if createCheckoutUrl() fails, the user gets no feedback
  2. No loading state — the button remains clickable during the async call, risking duplicate requests
🛡️ Suggested fix: add loading state and error handling
+  const [isLoading, setIsLoading] = useState(false);
+
   const handleCheckout = async () => {
-    const { checkoutUrl } = await createCheckoutUrl();
-    if (checkoutUrl) {
-      router.push(checkoutUrl);
+    setIsLoading(true);
+    try {
+      const { checkoutUrl } = await createCheckoutUrl();
+      if (checkoutUrl) {
+        router.push(checkoutUrl);
+      }
+    } catch (error) {
+      // Consider using a toast notification or error state
+      console.error("Failed to create checkout session:", error);
+    } finally {
+      setIsLoading(false);
     }
   };
   return (
     <Button
       onClick={handleCheckout}
-      disabled={disableButton}
+      disabled={disableButton || isLoading}
       className="w-full"
     >
-      Pay Dues
+      {isLoading ? "Loading..." : "Pay Dues"}
     </Button>

Why this matters: Users should receive clear feedback when something goes wrong with payment initialization, and preventing double-clicks avoids potential duplicate Stripe sessions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/blade/src/app/_components/dashboard/member-dashboard/payment/payment-button.tsx`
around lines 36 - 41, The handleCheckout function lacks error handling and a
loading state; update the component to add a local loading boolean (e.g.,
useState called isLoading) and guard the handler so if isLoading is true it
returns early, set isLoading=true before calling createCheckoutUrl(), wrap the
await createCheckoutUrl() call in try/catch to handle and surface errors (e.g.,
set an error state or call the existing toast/error UI) and in finally set
isLoading=false; also pass isLoading to the payment button to disable it and
show a spinner/disabled UI while the async request is in-flight so duplicate
requests and silent failures are prevented (refer to handleCheckout and
createCheckoutUrl).
🧹 Nitpick comments (7)
apps/blade/src/app/_components/dashboard/member-dashboard/payment/payment-button.tsx (1)

18-34: Consider computing disabled state inline instead of using useEffect.

The lint suppression is valid here since this is an initialization pattern, but you can eliminate both the useEffect and the suppression by computing the value directly. This is more idiomatic React—derive state from props when possible rather than syncing via effects.

♻️ Suggested refactor: compute inline
-  const [disableButton, setDisableButton] = useState<boolean>(false);
-
-  useEffect(() => {
-    const month = new Date().getMonth();
-
-    if (
-      (member.school !== "University of Central Florida" &&
-        member.school !== "Valencia College" &&
-        member.school !== "Seminole State College of Florida" &&
-        member.school !== "Rollins College" &&
-        member.school !== "DeVry University Orlando" &&
-        member.school !== "Johnson University Florida" &&
-        member.school !== "Full Sail University") ||
-      (month > 3 && month < 7) // disable during summer months
-    ) {
-      // eslint-disable-next-line react-hooks/set-state-in-effect
-      setDisableButton(true);
-    }
-  }, [member.school]);
+  const ELIGIBLE_SCHOOLS = [
+    "University of Central Florida",
+    "Valencia College",
+    "Seminole State College of Florida",
+    "Rollins College",
+    "DeVry University Orlando",
+    "Johnson University Florida",
+    "Full Sail University",
+  ] as const;
+
+  const month = new Date().getMonth();
+  const isIneligibleSchool = !ELIGIBLE_SCHOOLS.includes(member.school as typeof ELIGIBLE_SCHOOLS[number]);
+  const isSummerMonth = month > 3 && month < 7;
+  const disableButton = isIneligibleSchool || isSummerMonth;

Why this is better: Derived state avoids the extra render cycle that useEffect + setState causes, and the extracted ELIGIBLE_SCHOOLS array makes future maintenance easier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/blade/src/app/_components/dashboard/member-dashboard/payment/payment-button.tsx`
around lines 18 - 34, Replace the effect-based state sync in useEffect that
calls setDisableButton with a derived boolean computed inline: create a constant
array ELIGIBLE_SCHOOLS = [...] listing the allowed school names, compute const
isDisabled = !ELIGIBLE_SCHOOLS.includes(member.school) || (new Date().getMonth()
> 3 && new Date().getMonth() < 7) and use that value directly where you
currently read disableButton (or initialize state once from that expression if
you truly need state); remove the useEffect, its eslint-disable comment, and the
setDisableButton call so the disabled state is derived from member.school and
current month instead of being synced.
apps/blade/src/app/_components/forms/form-responder-client.tsx (1)

148-154: Good cleanup, but any cast remains — consider typing the mutation input.

Removing the unnecessary no-unsafe-assignment suppression is a nice refinement. However, per coding guidelines, any usage should still be flagged. The cast on line 152 bypasses type safety for the mutation payload.

A cleaner approach is to type the mutation's expected input so FormResponsePayload aligns directly, eliminating the need for the cast entirely.

♻️ Suggested improvement

If the tRPC mutation expects a specific shape, you can use a type assertion to that shape (or unknown with a guard) instead of any:

 const onSubmit = (payload: FormResponsePayload) => {
   submitResponse.mutate({
     form: formId,
-    // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-    responseData: payload as any,
+    responseData: payload as Record<string, unknown>,
   });
 };

Alternatively, if the mutation's input type can be updated in your tRPC router to accept FormResponsePayload directly, you can remove the cast altogether.

As per coding guidelines: "No TypeScript escape hatches: Flag usage of 'any' type... suggest 'unknown' with type guards, optional chaining (?.), or proper typing instead."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/forms/form-responder-client.tsx` around lines
148 - 154, The submit handler uses an any cast for mutation input — update the
mutation typing so FormResponsePayload is accepted without casting: adjust the
type of submitResponse (the tRPC mutation hook) to accept the correct input
shape (or export the mutation input type from the router and use it as the
generic for submitResponse) and then call submitResponse.mutate({ form: formId,
responseData: payload }) with payload typed as FormResponsePayload;
alternatively, if you cannot change the router, cast payload to the router's
exact mutation input type (or to unknown and validate via a guard) instead of
using any so type safety is preserved in onSubmit and the submitResponse.mutate
call.
apps/blade/src/app/_components/admin/forms/editor/client.tsx (2)

479-484: Use saveFormRef.current() instead of handleSaveForm in setTimeout.

The codebase establishes a ref pattern (lines 272–275) specifically to always invoke the latest handleSaveForm with fresh state. However, this setTimeout calls handleSaveForm directly, which captures state at the time reorderItems is defined—potentially stale after rapid reordering.

Recommended fix
      setQuestions(newQuestions);
      setInstructions(newInstructions);
-      setTimeout(() => handleSaveForm(), 100);
+      setTimeout(() => saveFormRef.current(), 100);
    },
-    [questions, instructions, handleSaveForm],
+    [questions, instructions],
  );

The same issue exists at line 513 in handleDragEnd:

      setQuestions(newQuestions);
      setInstructions(newInstructions);
-      setTimeout(() => handleSaveForm(), 100);
+      setTimeout(() => saveFormRef.current(), 100);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/admin/forms/editor/client.tsx` around lines
479 - 484, The setTimeout call inside reorderItems uses the captured
handleSaveForm (which can be stale); change it to call the ref wrapper so the
latest function is invoked: replace the setTimeout(() => handleSaveForm(), 100)
call with setTimeout(() => saveFormRef.current(), 100) and do the same
replacement for the analogous setTimeout in handleDragEnd so both use
saveFormRef.current() after setQuestions/setInstructions to ensure fresh state
is saved; references: reorderItems, handleDragEnd, handleSaveForm,
saveFormRef.current, setQuestions, setInstructions.

277-288: Selective ESLint suppression may be incomplete.

The suppression on line 281 silences react-hooks/set-state-in-effect for setFormTitle, but the same effect block immediately calls setFormDescription and setIsLoading without suppression. If the linter flags one, it likely flags all—meaning additional warnings may still appear.

Consider one of these approaches:

  1. Suppress at the effect level if the pattern is intentional for this test mock path
  2. Extract to a separate function called by the effect to centralize state hydration
  3. Use the query's select or placeholderData options to avoid the effect entirely
Example: Suppressing at the block level
        if (slug === "test-form") {
-          // eslint-disable-next-line react-hooks/set-state-in-effect
+          /* eslint-disable react-hooks/set-state-in-effect */
          setFormTitle("Test Form (Mock)");
          setFormDescription(
            "This is a mock form description for testing UI components.",
          );
          setIsLoading(false);
+          /* eslint-enable react-hooks/set-state-in-effect */
          return;
        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/admin/forms/editor/client.tsx` around lines
277 - 288, The eslint suppression currently only targets setFormTitle inside the
useEffect mock branch but setFormDescription and setIsLoading are also called
and may still trigger the react-hooks/set-state-in-effect rule; fix by either
moving the eslint-disable comment to cover the entire effect (place /*
eslint-disable react-hooks/set-state-in-effect */ immediately before the
useEffect and re-enable after), or extract the mock hydration into a separate
function (e.g., hydrateMockForm called from useEffect) and keep the single-line
suppression on that function, or eliminate the effect entirely by providing the
mock via the query's select/placeholderData—locate useEffect and the calls to
setFormTitle, setFormDescription, and setIsLoading to apply one of these
changes.
apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx (1)

61-65: Remove derived state and compute status values directly

Lines 37–38 declare state for hackerStatus and hackerStatusColor, which are purely derived from hacker?.status. The useEffect at lines 61–65 updates these values whenever hacker changes, requiring a lint suppression to hide the warning.

Since getStatusName() and getStatusColor() are pure functions with no side effects, compute these values directly during render instead. This eliminates the extra render cycle, removes the need for state and the lint suppression, and keeps related logic together.

Refactor
-import { useEffect, useState } from "react";
+// Remove unused React hooks
@@
-  const [hackerStatus, setHackerStatus] = useState<string | null>("");
-  const [hackerStatusColor, setHackerStatusColor] = useState<string>("");
@@
-  useEffect(() => {
-    // eslint-disable-next-line react-hooks/set-state-in-effect
-    setHackerStatus(getStatusName(hacker?.status));
-    setHackerStatusColor(getStatusColor(hacker?.status));
-  }, [hacker]);
+  const hackerStatus = getStatusName(hacker?.status);
+  const hackerStatusColor = getStatusColor(hacker?.status);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx`
around lines 61 - 65, The state variables hackerStatus and hackerStatusColor and
the useEffect updating them are unnecessary because they are derived from
hacker?.status; remove the state declarations and the useEffect (and the
eslint-disable comment) and instead compute values during render by calling
getStatusName(hacker?.status) and getStatusColor(hacker?.status) where needed
(e.g., replace uses of hackerStatus and hackerStatusColor with these direct
expressions), keeping hacker (the prop/variable) as the source of truth.
apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx (1)

139-142: Remove the lint suppression by adopting optimistic updates via tRPC cache manipulation

The react-hooks/set-state-in-effect lint warning masks a real pattern: hackerStatus and hackerStatusColor are set from two sources (query data via effect, optimistic updates in mutation handlers). Rather than removing the state entirely, use updateQueryData in the mutation's onSuccess to update the cache optimistically, then remove the manual setState calls.

Suggested refactor
  const confirmHacker = api.hackerMutation.confirmHacker.useMutation({
    async onSuccess() {
+     // Optimistically update the cache instead of manual setState
+     await utils.hackerQuery.getHacker.setData({}, (prev) => 
+       prev ? { ...prev, status: "confirmed" } : prev
+     );
      setIsConfirmOpen(true);
-     setHackerStatus("Confirmed");
-     setHackerStatusColor(getStatusColor("confirmed"));
      await utils.hackerQuery.getHacker.invalidate();
    },
  });

  const withdrawHacker = api.hackerMutation.withdrawHacker.useMutation({
    async onSuccess() {
+     // Optimistically update the cache instead of manual setState
+     await utils.hackerQuery.getHacker.setData({}, (prev) => 
+       prev ? { ...prev, status: "withdrawn" } : prev
+     );
      toast.success("You have withdrawn from the hackathon!");
-     setHackerStatus("Withdrawn");
-     setHackerStatusColor(getStatusColor("withdrawn"));
      await utils.hackerQuery.getHacker.invalidate();
    },
  });

The useEffect stays (removing the lint suppression is safe once state is no longer manually mutated in handlers), and the UI updates optimistically via the cache before re-fetching server truth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx`
around lines 139 - 142, The effect currently calls
setHackerStatus(getStatusName(hacker?.status)) and
setHackerStatusColor(getStatusColor(hacker?.status)) while mutation handlers
also mutate state, triggering the lint suppression; instead remove direct state
updates from mutation handlers and perform optimistic updates via tRPC cache by
calling trpcClient.<mutationKey>.useMutation(...).onSuccess =>
utils.hacker.getById.updateQueryData(id, draft => { draft.status = newStatus; })
(or equivalent) so that the useEffect can safely derive
hackerStatus/hackerStatusColor from hacker using getStatusName/getStatusColor;
remove the eslint-disable comment and any setHackerStatus/setHackerStatusColor
calls in mutation handlers, ensuring only the cache is updated optimistically in
the mutation onSuccess to reflect UI immediately.
apps/blade/src/app/_components/admin/hackathon/hackers/food-restrictions.tsx (1)

20-26: Remove derived state and the eslint suppression.

foodString is computed from hacker.foodAllergies and stored in state, requiring a useEffect hook and a lint suppression. Instead, compute it directly in the render phase.

Proposed refactor
-import { useEffect, useState } from "react";
+import { useState } from "react";
@@
   const [isOpen, setIsOpen] = useState<boolean>(false);
-  const [foodString, setFoodString] = useState("");
-
-  useEffect(() => {
-    // eslint-disable-next-line react-hooks/set-state-in-effect
-    setFoodString(hacker.foodAllergies?.replace(/,/g, ", ") || "");
-  }, [hacker.foodAllergies]);
+  const foodString =
+    hacker.foodAllergies
+      ?.split(",")
+      .map((item) => item.trim())
+      .filter(Boolean)
+      .join(", ") ?? "";

This removes an extra render cycle and eliminates the need for the lint suppression.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/admin/hackathon/hackers/food-restrictions.tsx`
around lines 20 - 26, Remove the derived state and eslint suppression by
deleting the useState for foodString, the setFoodString call, and the useEffect
(and its eslint-disable comment); instead compute the value inline in render
like: const foodString = hacker.foodAllergies?.replace(/,/g, ", ") || ""; also
remove any usages of setFoodString and keep isOpen state as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/blade/src/app/_components/forms/form-view-edit-client.tsx`:
- Around line 95-96: Remove the eslint-disable and the unsafe cast on the
responseData assignment (responseData: payload as any); instead construct
payload to match the mutation input type and assert it with TypeScript's
satisfies operator (e.g., const payload = { ... } satisfies UpdateClientInput or
the specific mutation input type used by your update client mutation), so
responseData: payload is fully typed; update the variable declarations/fields
that feed into payload to conform to that input type so the compiler catches
mismatches.

In `@apps/blade/src/app/_components/settings/hacker-profile-form.tsx`:
- Around line 248-252: Add an else branch to the conditional handling
hacker.foodAllergies so that when hacker.foodAllergies is empty or falsy you
clear both the state and ref: call setSelectedAllergies([]) and set
allergiesRef.current = [] in the else. Update the block around
setSelectedAllergies and allergiesRef.current (the code using
hacker.foodAllergies) to handle the empty case so stale badges are removed.

In `@apps/blade/src/app/_components/settings/member-profile-form.tsx`:
- Line 117: Remove the "@ts-expect-error" and fix the schema so its input types
match the form values: drop the .transform() calls on the schema fields for
gradYear and dob (keep them as string inputs), update the useForm generic/types
to use the schema's input type, change the initial value for gradYear to a
string (use String(initTermYear.year) instead of Number(initTermYear.year)) and
ensure member?.dob is passed as a string to the form, and keep the explicit
conversion logic in the submission handler (handleSubmit/onSubmit) where you
already defensively convert to numbers/dates.

In `@apps/blade/src/app/_components/shared/ToggleEditInput.tsx`:
- Around line 33-38: This component (ToggleEditInput) uses React hooks (useState
and useEffect) so add the Next.js App Router client directive 'use client' as
the first line of the file and then import useEffect/useState normally; remove
the incorrect eslint-disable comment referencing react-hooks/set-state-in-effect
(it's not a valid rule) and keep the effect that closes the toggle when submit
changes (setToggle(false) inside the useEffect that depends on submit) so
behavior is preserved.

---

Outside diff comments:
In
`@apps/blade/src/app/_components/dashboard/member-dashboard/payment/payment-button.tsx`:
- Around line 36-41: The handleCheckout function lacks error handling and a
loading state; update the component to add a local loading boolean (e.g.,
useState called isLoading) and guard the handler so if isLoading is true it
returns early, set isLoading=true before calling createCheckoutUrl(), wrap the
await createCheckoutUrl() call in try/catch to handle and surface errors (e.g.,
set an error state or call the existing toast/error UI) and in finally set
isLoading=false; also pass isLoading to the payment button to disable it and
show a spinner/disabled UI while the async request is in-flight so duplicate
requests and silent failures are prevented (refer to handleCheckout and
createCheckoutUrl).

---

Nitpick comments:
In `@apps/blade/src/app/_components/admin/forms/editor/client.tsx`:
- Around line 479-484: The setTimeout call inside reorderItems uses the captured
handleSaveForm (which can be stale); change it to call the ref wrapper so the
latest function is invoked: replace the setTimeout(() => handleSaveForm(), 100)
call with setTimeout(() => saveFormRef.current(), 100) and do the same
replacement for the analogous setTimeout in handleDragEnd so both use
saveFormRef.current() after setQuestions/setInstructions to ensure fresh state
is saved; references: reorderItems, handleDragEnd, handleSaveForm,
saveFormRef.current, setQuestions, setInstructions.
- Around line 277-288: The eslint suppression currently only targets
setFormTitle inside the useEffect mock branch but setFormDescription and
setIsLoading are also called and may still trigger the
react-hooks/set-state-in-effect rule; fix by either moving the eslint-disable
comment to cover the entire effect (place /* eslint-disable
react-hooks/set-state-in-effect */ immediately before the useEffect and
re-enable after), or extract the mock hydration into a separate function (e.g.,
hydrateMockForm called from useEffect) and keep the single-line suppression on
that function, or eliminate the effect entirely by providing the mock via the
query's select/placeholderData—locate useEffect and the calls to setFormTitle,
setFormDescription, and setIsLoading to apply one of these changes.

In
`@apps/blade/src/app/_components/admin/hackathon/hackers/food-restrictions.tsx`:
- Around line 20-26: Remove the derived state and eslint suppression by deleting
the useState for foodString, the setFoodString call, and the useEffect (and its
eslint-disable comment); instead compute the value inline in render like: const
foodString = hacker.foodAllergies?.replace(/,/g, ", ") || ""; also remove any
usages of setFoodString and keep isOpen state as-is.

In
`@apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx`:
- Around line 61-65: The state variables hackerStatus and hackerStatusColor and
the useEffect updating them are unnecessary because they are derived from
hacker?.status; remove the state declarations and the useEffect (and the
eslint-disable comment) and instead compute values during render by calling
getStatusName(hacker?.status) and getStatusColor(hacker?.status) where needed
(e.g., replace uses of hackerStatus and hackerStatusColor with these direct
expressions), keeping hacker (the prop/variable) as the source of truth.

In `@apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx`:
- Around line 139-142: The effect currently calls
setHackerStatus(getStatusName(hacker?.status)) and
setHackerStatusColor(getStatusColor(hacker?.status)) while mutation handlers
also mutate state, triggering the lint suppression; instead remove direct state
updates from mutation handlers and perform optimistic updates via tRPC cache by
calling trpcClient.<mutationKey>.useMutation(...).onSuccess =>
utils.hacker.getById.updateQueryData(id, draft => { draft.status = newStatus; })
(or equivalent) so that the useEffect can safely derive
hackerStatus/hackerStatusColor from hacker using getStatusName/getStatusColor;
remove the eslint-disable comment and any setHackerStatus/setHackerStatusColor
calls in mutation handlers, ensuring only the cache is updated optimistically in
the mutation onSuccess to reflect UI immediately.

In
`@apps/blade/src/app/_components/dashboard/member-dashboard/payment/payment-button.tsx`:
- Around line 18-34: Replace the effect-based state sync in useEffect that calls
setDisableButton with a derived boolean computed inline: create a constant array
ELIGIBLE_SCHOOLS = [...] listing the allowed school names, compute const
isDisabled = !ELIGIBLE_SCHOOLS.includes(member.school) || (new Date().getMonth()
> 3 && new Date().getMonth() < 7) and use that value directly where you
currently read disableButton (or initialize state once from that expression if
you truly need state); remove the useEffect, its eslint-disable comment, and the
setDisableButton call so the disabled state is derived from member.school and
current month instead of being synced.

In `@apps/blade/src/app/_components/forms/form-responder-client.tsx`:
- Around line 148-154: The submit handler uses an any cast for mutation input —
update the mutation typing so FormResponsePayload is accepted without casting:
adjust the type of submitResponse (the tRPC mutation hook) to accept the correct
input shape (or export the mutation input type from the router and use it as the
generic for submitResponse) and then call submitResponse.mutate({ form: formId,
responseData: payload }) with payload typed as FormResponsePayload;
alternatively, if you cannot change the router, cast payload to the router's
exact mutation input type (or to unknown and validate via a guard) instead of
using any so type safety is preserved in onSubmit and the submitResponse.mutate
call.

ℹ️ Review info

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13abad8 and bb27543.

📒 Files selected for processing (42)
  • apps/blade/src/app/_components/admin/charts/FirstTimeHackersPie.tsx
  • apps/blade/src/app/_components/admin/charts/GenderPie.tsx
  • apps/blade/src/app/_components/admin/charts/MajorBarChart.tsx
  • apps/blade/src/app/_components/admin/charts/RaceOrEthnicityPie.tsx
  • apps/blade/src/app/_components/admin/charts/SchoolBarChart.tsx
  • apps/blade/src/app/_components/admin/charts/SchoolYearPie.tsx
  • apps/blade/src/app/_components/admin/club/data/event-data/HowFound.tsx
  • apps/blade/src/app/_components/admin/club/data/event-data/TypePie.tsx
  • apps/blade/src/app/_components/admin/club/data/member-data/GenderPie.tsx
  • apps/blade/src/app/_components/admin/club/data/member-data/SchoolBarChart.tsx
  • apps/blade/src/app/_components/admin/club/data/member-data/ShirtSizePie.tsx
  • apps/blade/src/app/_components/admin/club/data/member-data/YearOfStudyPie.tsx
  • apps/blade/src/app/_components/admin/club/members/update-member.tsx
  • apps/blade/src/app/_components/admin/forms/editor/client.tsx
  • apps/blade/src/app/_components/admin/forms/homepage.tsx
  • apps/blade/src/app/_components/admin/forms/responses/FileUploadResponsesTable.tsx
  • apps/blade/src/app/_components/admin/forms/responses/ResponseHorizontalBarChart.tsx
  • apps/blade/src/app/_components/admin/forms/responses/ResponsesTable.tsx
  • apps/blade/src/app/_components/admin/hackathon/check-in/manual-entry-form.tsx
  • apps/blade/src/app/_components/admin/hackathon/data/HackathonDataContent.tsx
  • apps/blade/src/app/_components/admin/hackathon/data/LevelOfStudyPie.tsx
  • apps/blade/src/app/_components/admin/hackathon/data/ShirtSizePie.tsx
  • apps/blade/src/app/_components/admin/hackathon/hackers/food-restrictions.tsx
  • apps/blade/src/app/_components/admin/hackathon/hackers/hackers-table.tsx
  • apps/blade/src/app/_components/admin/hackathon/hackers/update-hacker.tsx
  • apps/blade/src/app/_components/admin/roles/roleedit.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx
  • apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx
  • apps/blade/src/app/_components/dashboard/hacker/hacker-application-form.tsx
  • apps/blade/src/app/_components/dashboard/member-dashboard/payment/payment-button.tsx
  • apps/blade/src/app/_components/forms/_hooks/useSubmissionSuccess.ts
  • apps/blade/src/app/_components/forms/form-responder-client.tsx
  • apps/blade/src/app/_components/forms/form-view-edit-client.tsx
  • apps/blade/src/app/_components/forms/shared/instruction-edit-card.tsx
  • apps/blade/src/app/_components/forms/shared/question-edit-card.tsx
  • apps/blade/src/app/_components/judge/projects-table.tsx
  • apps/blade/src/app/_components/settings/hacker-profile-form.tsx
  • apps/blade/src/app/_components/settings/member-profile-form.tsx
  • apps/blade/src/app/_components/shared/ToggleEditInput.tsx
  • apps/blade/src/app/api/membership/route.ts
✅ Files skipped from review due to trivial changes (5)
  • apps/blade/src/app/_components/admin/roles/roleedit.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx
  • apps/blade/src/app/_components/admin/hackathon/check-in/manual-entry-form.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/point-leaderboard.tsx
  • apps/blade/src/app/_components/admin/hackathon/data/HackathonDataContent.tsx
🚧 Files skipped from review as they are similar to previous changes (11)
  • apps/blade/src/app/_components/admin/charts/FirstTimeHackersPie.tsx
  • apps/blade/src/app/_components/admin/club/data/member-data/ShirtSizePie.tsx
  • apps/blade/src/app/_components/admin/club/data/event-data/HowFound.tsx
  • apps/blade/src/app/_components/admin/club/data/event-data/TypePie.tsx
  • apps/blade/src/app/_components/dashboard/hacker/hacker-application-form.tsx
  • apps/blade/src/app/_components/admin/charts/SchoolYearPie.tsx
  • apps/blade/src/app/_components/admin/club/members/update-member.tsx
  • apps/blade/src/app/_components/admin/charts/GenderPie.tsx
  • apps/blade/src/app/_components/admin/club/data/member-data/GenderPie.tsx
  • apps/blade/src/app/_components/admin/hackathon/data/ShirtSizePie.tsx
  • apps/blade/src/app/api/membership/route.ts

Comment on lines +95 to 96
// eslint-disable-next-line @typescript-eslint/no-explicit-any
responseData: payload as any,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, examine the file at the flagged lines
cat -n apps/blade/src/app/_components/forms/form-view-edit-client.tsx | sed -n '85,105p'

Repository: KnightHacks/forge

Length of output: 682


🏁 Script executed:

# Find the editResponse mutation import/definition
rg -n 'editResponse|api\.forms\.editResponse' apps/blade/src/app/_components/forms/form-view-edit-client.tsx -B2 -A2

Repository: KnightHacks/forge

Length of output: 578


🏁 Script executed:

# Search for any TypeScript escape hatches in this file
rg -n '@ts-ignore|@ts-expect-error|as any|![\.\)]' apps/blade/src/app/_components/forms/form-view-edit-client.tsx

Repository: KnightHacks/forge

Length of output: 99


🏁 Script executed:

# Locate the api/router files to understand the mutation type signature
fd -t f 'forms' --path '*/server/*' --path '*/api/*' | head -20

Repository: KnightHacks/forge

Length of output: 294


🏁 Script executed:

# Find the forms router/trpc definition
find apps -name '*.ts' -type f | xargs rg 'editResponse' | grep -E '(router|procedure|createTRPCRouter)' -B2 -A2

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

# Search for api/forms files
fd -type f 'forms' apps/

Repository: KnightHacks/forge

Length of output: 231


🏁 Script executed:

# Look for trpc router definitions
rg -n 'editResponse' --type ts --type tsx -l | head -10

Repository: KnightHacks/forge

Length of output: 88


🏁 Script executed:

# Examine the api structure
find apps -type f -name '*.ts' | xargs grep -l 'createTRPCRouter\|router\(' | grep -i form | head -5

Repository: KnightHacks/forge

Length of output: 84


🏁 Script executed:

# Find files containing editResponse
rg 'editResponse' -l apps/

Repository: KnightHacks/forge

Length of output: 123


🏁 Script executed:

# Look for trpc router structure
find apps -type d -name 'api' -o -type d -name 'server'

Repository: KnightHacks/forge

Length of output: 83


🏁 Script executed:

# Search for forms router/procedure
rg -n 'editResponse' apps/ -B3 -A3 | head -40

Repository: KnightHacks/forge

Length of output: 2075


🏁 Script executed:

# Look in api directory
find apps/blade/src/app/api -type f -name '*.ts'

Repository: KnightHacks/forge

Length of output: 237


🏁 Script executed:

# Search for where api is imported in the file
rg -n 'import.*api' apps/blade/src/app/_components/forms/form-view-edit-client.tsx

Repository: KnightHacks/forge

Length of output: 98


🏁 Script executed:

# Look for forms procedure/router
rg -n 'forms.*editResponse|editResponse.*procedure' apps/

Repository: KnightHacks/forge

Length of output: 186


🏁 Script executed:

# Find trpc directory
find apps -type d -name 'trpc'

Repository: KnightHacks/forge

Length of output: 151


🏁 Script executed:

# Look for router files that might define forms
find apps -type f -name '*.ts' | xargs rg 'router.*forms|forms.*router' | head -10

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

# Search for procedure definitions with editResponse
rg -n 'procedure.*editResponse|editResponse.*mutation' apps/ -B2 -A5

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

# Look for all files in trpc directory
find apps -path '*/trpc/*' -type f

Repository: KnightHacks/forge

Length of output: 329


🏁 Script executed:

# Look at the server.ts file for router structure
cat apps/blade/src/trpc/server.ts

Repository: KnightHacks/forge

Length of output: 974


🏁 Script executed:

# Search for router directory with forms
find apps/blade -type d -name 'router' -o -type d -name 'routers'

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

# Search for files containing 'forms' as a router
rg -n 'forms:' apps/ -B2 -A5

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

# Find packages directory
find . -type d -name 'packages' -o -type d -name 'api' | head -10

Repository: KnightHacks/forge

Length of output: 111


🏁 Script executed:

# Look for `@forge/api` in package.json or tsconfig
rg '@forge/api' --type json | head -5

Repository: KnightHacks/forge

Length of output: 343


🏁 Script executed:

# Find where `@forge/api` points to
find . -name 'tsconfig.json' -o -name 'turbo.json' | head -5

Repository: KnightHacks/forge

Length of output: 186


🏁 Script executed:

# Search for forms router definition across all ts files
rg 'forms.*:.*procedure|editResponse' . --type ts -l | head -10

Repository: KnightHacks/forge

Length of output: 161


🏁 Script executed:

# Examine the forms router
cat packages/api/src/routers/forms.ts

Repository: KnightHacks/forge

Length of output: 38154


Remove the as any escape hatch and use type-safe input construction.

Line 96 bypasses type safety with payload as any, and the eslint suppression at line 95 masks the actual typing issue. Use satisfies with the mutation input type to catch type mismatches at compile time instead of hiding them.

✅ Typed fix
   const onSubmit = (payload: FormResponsePayload) => {
-    editResponse.mutate({
-      id: responseId,
-      // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-      responseData: payload as any,
-    });
+    const input = {
+      id: responseId,
+      responseData: payload,
+    } satisfies Parameters<typeof editResponse.mutate>[0];
+    editResponse.mutate(input);
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/forms/form-view-edit-client.tsx` around lines
95 - 96, Remove the eslint-disable and the unsafe cast on the responseData
assignment (responseData: payload as any); instead construct payload to match
the mutation input type and assert it with TypeScript's satisfies operator
(e.g., const payload = { ... } satisfies UpdateClientInput or the specific
mutation input type used by your update client mutation), so responseData:
payload is fully typed; update the variable declarations/fields that feed into
payload to conform to that input type so the compiler catches mismatches.

Comment on lines 248 to 252
if (hacker.foodAllergies) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setSelectedAllergies(hacker.foodAllergies.split(","));
allergiesRef.current = hacker.foodAllergies.split(",");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

wc -l apps/blade/src/app/_components/settings/hacker-profile-form.tsx

Repository: KnightHacks/forge

Length of output: 128


🏁 Script executed:

sed -n '200,300p' apps/blade/src/app/_components/settings/hacker-profile-form.tsx | cat -n

Repository: KnightHacks/forge

Length of output: 4117


🏁 Script executed:

# Get more context around the target lines and the useEffect hook
sed -n '220,280p' apps/blade/src/app/_components/settings/hacker-profile-form.tsx | cat -n

Repository: KnightHacks/forge

Length of output: 2514


🏁 Script executed:

# Look for other setSelectedAllergies or allergiesRef usage
rg 'setSelectedAllergies|allergiesRef|foodAllergies' apps/blade/src/app/_components/settings/hacker-profile-form.tsx -n

Repository: KnightHacks/forge

Length of output: 797


🏁 Script executed:

# Check the effect dependencies and component structure
sed -n '1,80p' apps/blade/src/app/_components/settings/hacker-profile-form.tsx | cat -n

Repository: KnightHacks/forge

Length of output: 2924


Reset allergy state when backend foodAllergies is empty.

When hacker.foodAllergies transitions from a value to empty, the form field resets but selectedAllergies state and allergiesRef don't. This causes stale badges to remain rendered. Add an else block to reset both when the data is empty.

💡 Proposed fix
    if (hacker.foodAllergies) {
      // eslint-disable-next-line react-hooks/set-state-in-effect
      setSelectedAllergies(hacker.foodAllergies.split(","));
      allergiesRef.current = hacker.foodAllergies.split(",");
+   } else {
+     // eslint-disable-next-line react-hooks/set-state-in-effect
+     setSelectedAllergies([]);
+     allergiesRef.current = [];
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (hacker.foodAllergies) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setSelectedAllergies(hacker.foodAllergies.split(","));
allergiesRef.current = hacker.foodAllergies.split(",");
}
if (hacker.foodAllergies) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setSelectedAllergies(hacker.foodAllergies.split(","));
allergiesRef.current = hacker.foodAllergies.split(",");
} else {
// eslint-disable-next-line react-hooks/set-state-in-effect
setSelectedAllergies([]);
allergiesRef.current = [];
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/settings/hacker-profile-form.tsx` around lines
248 - 252, Add an else branch to the conditional handling hacker.foodAllergies
so that when hacker.foodAllergies is empty or falsy you clear both the state and
ref: call setSelectedAllergies([]) and set allergiesRef.current = [] in the
else. Update the block around setSelectedAllergies and allergiesRef.current (the
code using hacker.foodAllergies) to handle the empty case so stale badges are
removed.

};

const form = useForm({
// @ts-expect-error -- schema uses .transform() so input≠output; ZodType<TIn,TIn> in useForm can't represent this
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/blade/src/app/_components/settings/member-profile-form.tsx | head -270 | tail -160

Repository: KnightHacks/forge

Length of output: 6605


🏁 Script executed:

cat -n apps/blade/src/app/_components/settings/member-profile-form.tsx | sed -n '280,350p'

Repository: KnightHacks/forge

Length of output: 2827


Remove the TypeScript suppression and align schema input types with form values.

The @ts-expect-error at line 117 hides a real type mismatch: the schema expects string inputs for gradYear and dob, but line 252 provides Number(initTermYear.year) and line 250 provides the raw member?.dob (likely already a string/date). This is a direct violation of the coding guideline flagging TypeScript escape hatches.

The fix is to remove .transform() from the schema (keeping inputs as-is) and handle conversions explicitly at submission time, where line 338 already does defensive conversion. This makes types transparent and prevents future mismatches.

Suggested changes
-    // `@ts-expect-error` -- schema uses .transform() so input≠output; ZodType<TIn,TIn> in useForm can't represent this
     const form = useForm({
       schema: InsertMemberSchema.omit({
@@
-      dob: z
-        .string()
-        .pipe(z.coerce.date())
-        .refine(
-          (date) => {
-            const today = new Date();
-            const age = calculateAge(date, today);
-            return age >= 16;
-          },
-          {
-            message:
-              "You must be at least 16 years old to be a Knight Hacks member",
-          },
-        )
-        .transform((date) => date.toISOString()),
+      dob: z.string().refine(
+        (value) => {
+          const date = new Date(value);
+          if (Number.isNaN(date.getTime())) return false;
+          return calculateAge(date, new Date()) >= 16;
+        },
+        {
+          message:
+            "You must be at least 16 years old to be a Knight Hacks member",
+        },
+      ),
@@
-      gradYear: z
-        .string()
-        .regex(/^\d{4}$/, "Enter a 4‑digit year")
-        .transform(Number),
+      gradYear: z.string().regex(/^\d{4}$/, "Enter a 4‑digit year"),
@@
-      gradYear: Number(initTermYear.year),
+      gradYear: initTermYear.year,
@@
               updateMember.mutate({
                 id: member.id,
                 ...values,
+                dob: new Date(values.dob).toISOString(),
                 resumeUrl,
                 profilePictureUrl,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/settings/member-profile-form.tsx` at line 117,
Remove the "@ts-expect-error" and fix the schema so its input types match the
form values: drop the .transform() calls on the schema fields for gradYear and
dob (keep them as string inputs), update the useForm generic/types to use the
schema's input type, change the initial value for gradYear to a string (use
String(initTermYear.year) instead of Number(initTermYear.year)) and ensure
member?.dob is passed as a string to the form, and keep the explicit conversion
logic in the submission handler (handleSubmit/onSubmit) where you already
defensively convert to numbers/dates.

Comment on lines 33 to 38
useEffect(() => {
if (submit) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setToggle(false);
}
}, [submit]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read the file to see the full context and check for 'use client'
head -50 apps/blade/src/app/_components/shared/ToggleEditInput.tsx

Repository: KnightHacks/forge

Length of output: 1116


🏁 Script executed:

# Search for the ESLint rule in config files and rule definitions
rg "set-state-in-effect" -g "*.{js,ts,json,yml,yaml}"

Repository: KnightHacks/forge

Length of output: 194


🏁 Script executed:

# Check ESLint configuration files
fd -e js -e ts -e json -e yaml -e yml -g "*eslint*" -type f | head -20

Repository: KnightHacks/forge

Length of output: 231


🏁 Script executed:

# Look at the ESLint config for any custom rules or plugins
find . -maxdepth 3 -type f \( -name "eslint.config.*" -o -name ".eslintrc*" \) | head -10

Repository: KnightHacks/forge

Length of output: 370


🏁 Script executed:

# Read the full ToggleEditInput.tsx file
wc -l apps/blade/src/app/_components/shared/ToggleEditInput.tsx

Repository: KnightHacks/forge

Length of output: 122


🏁 Script executed:

# Check the full file to see if 'use client' exists
cat apps/blade/src/app/_components/shared/ToggleEditInput.tsx

Repository: KnightHacks/forge

Length of output: 3173


🏁 Script executed:

# Read the blade ESLint config to understand the rule
cat apps/blade/eslint.config.js

Repository: KnightHacks/forge

Length of output: 520


🏁 Script executed:

# Search for where this rule might be defined or come from
rg "react-hooks" apps/blade/eslint.config.js -A 5 -B 5

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

# Find the `@forge/eslint-config` package location
fd -type d -name "eslint-config" 2>/dev/null | head -5

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

# List files in the eslint-config package
ls -la packages/eslint-config/ 2>/dev/null || find . -path "*eslint-config*" -name "*.js" | head -10

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

# Check if the rule is from a known plugin
rg "set-state-in-effect" . --type js

Repository: KnightHacks/forge

Length of output: 43


🏁 Script executed:

# Search for plugin configurations
rg "react-hooks" packages/eslint-config/ --type js -A 3 -B 3 2>/dev/null || rg "eslint-plugin-react-hooks" . --type js --type json

Repository: KnightHacks/forge

Length of output: 212


🏁 Script executed:

# Read the react.js config to see how the plugin is configured
cat ./tooling/eslint/react.js

Repository: KnightHacks/forge

Length of output: 569


🏁 Script executed:

# Check the eslint-plugin-react-hooks package.json to understand what it provides
cat ./tooling/eslint/package.json | jq '.dependencies, .devDependencies' 2>/dev/null || grep -A 5 -B 5 "react-hooks" ./tooling/eslint/package.json

Repository: KnightHacks/forge

Length of output: 544


🏁 Script executed:

# Search online documentation - the standard rules for eslint-plugin-react-hooks are well-known
# Let me check if there are any custom rule definitions or if this might be a typo
rg "set-state" . --type js --type ts | head -20

Repository: KnightHacks/forge

Length of output: 2628


Add 'use client' directive—this component uses React hooks in a Next.js App Router.

This file uses useState and useEffect, which only work in client components. Add 'use client' at the top:

'use client';

import { useEffect, useState } from "react";
// ... rest of imports

Also, the eslint-disable comment references react-hooks/set-state-in-effect, which is not a standard rule in eslint-plugin-react-hooks. Remove the suppression comment once the 'use client' directive is added—it's likely suppressing a non-existent rule.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/blade/src/app/_components/shared/ToggleEditInput.tsx` around lines 33 -
38, This component (ToggleEditInput) uses React hooks (useState and useEffect)
so add the Next.js App Router client directive 'use client' as the first line of
the file and then import useEffect/useState normally; remove the incorrect
eslint-disable comment referencing react-hooks/set-state-in-effect (it's not a
valid rule) and keep the effect that closes the toggle when submit changes
(setToggle(false) inside the useEffect that depends on submit) so behavior is
preserved.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
apps/club/src/app/_components/links/button.tsx (2)

33-33: ⚠️ Potential issue | 🟡 Minor

Replace transition-color with transition-colors.

Line 33 and Line 56 use a non-standard Tailwind class, so the intended color transition won’t apply.

Proposed fix
- className="font-pragati transition-color bg-linear-to-br group relative inline-flex transform items-center gap-2 rounded-[200px] border border-[`#D8B4FE`] bg-transparent px-14 py-3 text-[12px] font-normal leading-normal tracking-[1px] text-white duration-100 hover:bg-[`#D8B4FE`] hover:text-[`#0F172A`] md:px-24 md:text-[13px] md:font-bold md:tracking-[.8px]"
+ className="font-pragati transition-colors bg-linear-to-br group relative inline-flex transform items-center gap-2 rounded-[200px] border border-[`#D8B4FE`] bg-transparent px-14 py-3 text-[12px] font-normal leading-normal tracking-[1px] text-white duration-100 hover:bg-[`#D8B4FE`] hover:text-[`#0F172A`] md:px-24 md:text-[13px] md:font-bold md:tracking-[.8px]"
Tailwind CSS v4 docs: Is `transition-color` a valid utility class, and is `transition-colors` the correct utility for color property transitions?

Also applies to: 56-56

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/club/src/app/_components/links/button.tsx` at line 33, Update the
Tailwind transition utility in the button component: replace the nonstandard
class "transition-color" with the correct "transition-colors" wherever it
appears in the className strings in
apps/club/src/app/_components/links/button.tsx (both occurrences around the
inline className in the Button component) so the color transition utilities are
applied correctly.

45-45: ⚠️ Potential issue | 🟡 Minor

MenuHorizontalSVG hover styles should target stroke, not fill.

The referenced SVG paths are stroke-based, so group-hover:**:fill[...] and fill-current won’t drive the intended color change.

Proposed fix
- className="md:right-15 **:transition-colors **:duration-100 group-hover:**:fill-[`#0F172A`] absolute right-6 h-4 w-4 fill-current md:h-5 md:w-5"
+ className="md:right-15 **:transition-colors **:duration-100 **:stroke-[`#D7B4FE`] group-hover:**:stroke-[`#0F172A`] absolute right-6 h-4 w-4 md:h-5 md:w-5"
#!/bin/bash
# Verify class usage in the button component
rg -n 'MenuHorizontalSVG|group-hover:\*\*:fill|fill-current|group-hover:\*\*:stroke|stroke-current' apps/club/src/app/_components/links/button.tsx

# Verify stroke/fill attributes in the SVG asset
rg -n 'stroke=|fill=' apps/club/src/app/_components/links/assets/menu-horizontal.tsx

Also applies to: 68-68

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/club/src/app/_components/links/button.tsx` at line 45, The hover color
classes on MenuHorizontalSVG are using fill utilities but the SVG uses strokes;
update the className on the MenuHorizontalSVG instances to use stroke utilities
instead of fill (replace occurrences like "group-hover:**:fill-[`#0F172A`]" with
"group-hover:**:stroke-[`#0F172A`]" and change "fill-current" to
"stroke-current"), ensuring any size/position classes remain unchanged so hover
color is driven by stroke rather than fill.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/club/src/app/_components/links/button.tsx`:
- Line 33: Update the Tailwind transition utility in the button component:
replace the nonstandard class "transition-color" with the correct
"transition-colors" wherever it appears in the className strings in
apps/club/src/app/_components/links/button.tsx (both occurrences around the
inline className in the Button component) so the color transition utilities are
applied correctly.
- Line 45: The hover color classes on MenuHorizontalSVG are using fill utilities
but the SVG uses strokes; update the className on the MenuHorizontalSVG
instances to use stroke utilities instead of fill (replace occurrences like
"group-hover:**:fill-[`#0F172A`]" with "group-hover:**:stroke-[`#0F172A`]" and
change "fill-current" to "stroke-current"), ensuring any size/position classes
remain unchanged so hover color is driven by stroke rather than fill.

ℹ️ Review info

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc0247 and 377e2df.

📒 Files selected for processing (8)
  • apps/2025/src/app/_components/navbar/Navbar.tsx
  • apps/club/src/app/_components/landing/calendar.tsx
  • apps/club/src/app/_components/landing/hero-assets/typing-text.tsx
  • apps/club/src/app/_components/links/button.tsx
  • packages/api/src/routers/csv-importer.ts
  • packages/api/src/routers/email.ts
  • packages/api/src/routers/hackers/mutations.ts
  • packages/api/src/utils.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/club/src/app/_components/landing/calendar.tsx
  • packages/api/src/routers/email.ts
  • packages/api/src/utils.ts

Copy link
Contributor

@DVidal1205 DVidal1205 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@DVidal1205 DVidal1205 added this pull request to the merge queue Mar 1, 2026
Merged via the queue into main with commit 49052cc Mar 1, 2026
8 checks passed
@DVidal1205 DVidal1205 deleted the update-package-versions branch March 1, 2026 20:19
DVidal1205 added a commit that referenced this pull request Mar 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file Global Change modifies code for the entire repository Major Big change - 2+ reviewers required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update package versions

2 participants