Skip to content

Add Tailwind CSS setup, theme system, and UI components / flow#13

Open
Casjb wants to merge 36 commits into
mainfrom
finishing-wireframe
Open

Add Tailwind CSS setup, theme system, and UI components / flow#13
Casjb wants to merge 36 commits into
mainfrom
finishing-wireframe

Conversation

@Casjb
Copy link
Copy Markdown
Owner

@Casjb Casjb commented Apr 8, 2026

Summary by CodeRabbit

  • New Features

    • Global error banner with dismiss and centralized error handling
    • Global loading overlay with optional opaque mode and themed spinner
    • Reusable context menu and generic paginated resource list with URL-driven paging
    • Theme preview page, theme toggle, and Pokéball icon that reacts to theme
    • Tailwind/PostCSS integration and CSS-variable theming
  • Bug Fixes

    • More robust loading/error flows and safer network error handling
    • Improved create/rename validation and uniqueness checks
  • Documentation

    • Added frontend fixlist/notes document

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

Adds Tailwind/PostCSS, a CSS-variable theme (dark/light), global error and loading Redux slices with providers/UI, new generic UI components and hooks (resource list, extendable context menu, global loader), numerous helper refactors, and a ThemeTest page and route.

Changes

Cohort / File(s) Summary
Tailwind & PostCSS
frontend/package.json, frontend/postcss.config.js, frontend/tailwind.config.ts, frontend/src/index.css
Added Tailwind/PostCSS devDependencies and PostCSS config; added Tailwind config; replaced custom global CSS with Tailwind directives.
Theme styles & runtime
frontend/src/styles/theme.css, frontend/src/Main.tsx, frontend/src/components/Pokeball.tsx, frontend/src/pages/test/ThemeTest.tsx
Added CSS-variable theme (dark default, .light overrides), theme init using prefers-color-scheme, Pokeball component observing root light class, and ThemeTest page with toggle and UI samples.
Redux: loading & error
frontend/src/redux/loadingSlice.ts, frontend/src/redux/errorSlice.ts, frontend/src/redux/store.ts
Added errorSlice; extended loadingSlice with opaque and payload for start; registered error reducer in store; exported selectors.
Global error helpers & UI
frontend/src/helpers/exports/exportError.ts, frontend/src/components/error/ErrorBanner.tsx, frontend/src/providers/ErrorProvider.tsx
Added throwError/clearError/getError helpers; added dismissible ErrorBanner and ErrorProvider that renders banner from Redux state.
Global loader & providers
frontend/src/components/load/GlobalLoader.tsx, frontend/src/components/load/ShowLoader.tsx, frontend/src/providers/LoadingProvider.tsx, frontend/src/components/ProtectedRoutes.tsx, frontend/src/helpers/exports/exportLoading.ts
Extracted GlobalLoader (optional opaque); ShowLoader uses GlobalLoader by default and removes extra wrapper; LoadingProvider consumes opaque and manages delayed visibility; ProtectedRoutes now uses exported start/stop loading; startLoading accepts opaque.
Generic components & hooks
frontend/src/components/api/DisplayResourceGeneric.tsx, frontend/src/components/context/ExtendableContextMenu.tsx, frontend/src/hooks/load/useResource.ts
Added DisplayResourceGeneric (URL-driven paging, context-menu support), ExtendableContextMenu (text & validated input fields, async validation and submit), and useResource hook returning items/totalPages/load/ready.
Leagues features & helpers
frontend/src/components/leagues/LeagueContextMenu.tsx, frontend/src/pages/leagues/Leagues.tsx, frontend/src/pages/leagues/League.tsx, frontend/src/helpers/leagues/renameLeague.ts, frontend/src/helpers/leagues/fetchLeaguesIndex.ts, frontend/src/helpers/leagues/fetchLeaguesSpecific.ts
Replaced inline league context menu with generic ContextMenu and added rename flow with uniqueness check; Leagues list now uses DisplayResourceGeneric and URL paging; fetch helpers call throwError and return null on failure; added renameLeague helper.
Resource creation & validation
frontend/src/helpers/manageResource/createNewResource.ts, frontend/src/helpers/validation/checkUniqueness.ts
createNewResource now uses try/catch/finally and accepts sync/async onReload; checkUniqueness removed loader integration and returns false on non-OK responses.
Removed / replaced
frontend/src/components/load/LoadResourceIndex.tsx
Removed legacy LoadResourceIndex component; functionality replaced by DisplayResourceGeneric.
App & routing
frontend/src/App.tsx, frontend/src/pages/test/Test.tsx
Wrapped app with new ErrorProvider; added DEV-only /Test/Theme route and Test page navigation to it.
Modals & forms
frontend/src/components/modals/CreationModal.tsx
Per-field validation and errors, async uniqueness checks during input, submit error handling, onReload widened to accept sync or Promise.
Docs / TODOs
frontend/fixme.md
Added fixme checklist documenting bugs, defensive checks, and polish items across frontend files.

Sequence Diagram(s)

sequenceDiagram
    participant Page
    participant DisplayResourceGeneric
    participant useResource
    participant FetchHelper
    participant Redux

    Page->>DisplayResourceGeneric: render(fetchFn, page)
    DisplayResourceGeneric->>useResource: init(fetchFn, page)
    DisplayResourceGeneric->>useResource: call load()
    useResource->>FetchHelper: fetchFn(page)
    alt success
        FetchHelper-->>useResource: response(items,_meta)
        useResource->>DisplayResourceGeneric: {items,totalPages}
        DisplayResourceGeneric->>Page: render list & pagination
    else failure
        FetchHelper->>Redux: call throwError(msg)
        Redux->>Redux: setError + stopLoad
        FetchHelper-->>useResource: null
        useResource->>DisplayResourceGeneric: ready=false / empty
    end
Loading
sequenceDiagram
    participant Component
    participant Redux
    participant ErrorProvider
    participant ErrorBanner

    Component->>Redux: call throwError(msg)
    activate Redux
    Redux->>Redux: dispatch setError(msg)
    Redux->>Redux: dispatch stopLoad()
    deactivate Redux

    ErrorProvider->>Redux: useSelector(selectError)
    Redux-->>ErrorProvider: error state
    ErrorProvider->>ErrorBanner: render(message)
    ErrorBanner->>Component: visible banner
    Component->>ErrorBanner: click ✕
    ErrorBanner->>Redux: clearError()
    activate Redux
    Redux->>Redux: dispatch resetError()
    deactivate Redux
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I hopped through styles and loader spins,

variables set where daylight begins.
Errors now sing from Redux hill,
menus that validate, inputs still.
Tailwind flowers — a cheerful thrill.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding Tailwind CSS setup (package.json, postcss.config.js, tailwind.config.ts), a comprehensive theme system (theme.css with CSS variables, light/dark mode support), and multiple UI components and flows (ErrorProvider, GlobalLoader, ContextMenu, DisplayResourceGeneric, etc.).

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch finishing-wireframe

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

Copy link
Copy Markdown

@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: 19

Caution

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

⚠️ Outside diff range comments (2)
frontend/src/helpers/validation/checkUniqueness.ts (1)

6-13: ⚠️ Potential issue | 🟠 Major

Validate the API response shape before returning uniqueness.

On Line 12, the function returns raw JSON without checking response.ok or type. If the backend returns an error object (truthy), callers can treat it as unique=true and allow invalid input.

Suggested fix
   try {
     const response = await fetch(
       `${BASE_URL}/${tableName}/check-unique?name=${encodeURIComponent(fieldName)}`,
       {
         credentials: 'include',
       }
     )
-    return await response.json()
+    if (!response.ok) return false
+
+    const data: unknown = await response.json()
+    if (typeof data === 'boolean') return data
+    if (
+      typeof data === 'object' &&
+      data !== null &&
+      'unique' in data &&
+      typeof (data as { unique: unknown }).unique === 'boolean'
+    ) {
+      return (data as { unique: boolean }).unique
+    }
+    return false
   } catch (err) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/helpers/validation/checkUniqueness.ts` around lines 6 - 13, The
function that fetches `${BASE_URL}/${tableName}/check-unique` currently returns
raw JSON without checking response status or verifying the shape; update the
fetch handling in checkUniqueness (use the existing response variable) to first
verify response.ok, parse the JSON, and validate that the parsed object contains
the expected boolean field (e.g., unique or isUnique) before returning; if
response.ok is false or the shape is invalid, throw or return a clear
error/false result so callers cannot treat an error object as unique.
frontend/src/helpers/manageResource/createNewResource.ts (1)

17-37: ⚠️ Potential issue | 🟠 Major

Do not swallow create failures before running onReload.

Current flow logs errors and continues, so failed creates still execute onReload and appear successful to callers.

Suggested fix
 import { BASE_URL } from '../exports/exportEnv.ts'
+import { throwError } from '../exports/exportError.ts'
@@
 export const createNewResource = async ({ tableName, data, onReload }: ResourceCreation) => {
-  await fetch(`${BASE_URL}/${tableName}/create`, {
-    method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
-    body: JSON.stringify({
-      name: data.name,
-    }),
-    credentials: 'include',
-  })
-    .then(res => res.json())
-    .then(data => {
-      console.log(data)
-    })
-    .catch(err => console.log(err))
+  try {
+    const response = await fetch(`${BASE_URL}/${tableName}/create`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ name: data.name }),
+      credentials: 'include',
+    })
+
+    if (!response.ok) {
+      throw new Error(`Create failed (${response.status})`)
+    }
+
+    await response.json()
+  } catch (err: any) {
+    throwError(`Failed to create resource: ${err.message}`)
+    return
+  }
@@
   if (onReload) {
     await onReload()
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/helpers/manageResource/createNewResource.ts` around lines 17 -
37, The createNewResource function currently swallows fetch errors and always
calls onReload; update createNewResource so it checks the fetch response (use
response.ok or parsed payload status) and throws or rethrows on failure instead
of just console.logging, and only call await onReload() after a successful
create. Locate the fetch block in createNewResource, ensure non-2xx responses
and caught exceptions are propagated (or return an error) so callers know the
create failed, and keep onReload invocation conditional on a successful create.
🧹 Nitpick comments (6)
frontend/src/Main.tsx (1)

10-14: Guard module-scope DOM access for non-browser execution.

On Lines 10-14, window/document are accessed during module evaluation. A simple runtime guard makes this safer for SSR-like or DOM-less test environments.

Suggested hardening
-const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
-// only apply light if system explicitly prefers it
-if (!prefersDark) {
-  document.documentElement.classList.add('light')
-}
+if (typeof window !== 'undefined' && typeof document !== 'undefined') {
+  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
+  // only apply light if system explicitly prefers it
+  if (!prefersDark) {
+    document.documentElement.classList.add('light')
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/Main.tsx` around lines 10 - 14, The module currently accesses
window and document at top-level (prefersDark using window.matchMedia and
document.documentElement.classList.add('light')), which breaks in non-browser
environments; wrap that logic in a runtime guard such as checking typeof window
!== 'undefined' && typeof document !== 'undefined' (and that window.matchMedia
exists) before evaluating prefersDark or mutating document, so the code only
runs in browsers and is safe for SSR/tests.
frontend/src/App.tsx (1)

37-37: Gate /Test/Theme behind a dev-only condition.

This route is marked temporary; consider only registering it in development builds to avoid exposing internal test surfaces.

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

In `@frontend/src/App.tsx` at line 37, The Test Theme route is currently always
registered; wrap the Route that renders ThemeTest (the JSX line Route
path="/Test/Theme" element={<ThemeTest />} inside App or the component that
builds routes) in a dev-only conditional so it only mounts in development builds
(e.g., guard with process.env.NODE_ENV === 'development' or your build's dev
flag like import.meta.env.DEV). Keep the same Route JSX but render it only when
the dev flag is true so production builds do not expose the /Test/Theme test
page.
frontend/src/components/load/GlobalLoader.tsx (1)

16-16: Prefer the theme overlay token for consistency.

On Line 16, consider using var(--color-bg-overlay) instead of hardcoded rgba(0, 0, 0, 0.5) so loader overlay stays fully theme-driven.

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

In `@frontend/src/components/load/GlobalLoader.tsx` at line 16, Replace the
hardcoded overlay color in the GlobalLoader component by using the theme token:
when computing the backgroundColor (the property currently set to opaque ?
'var(--color-bg)' : 'rgba(0, 0, 0, 0.5)'), swap the hardcoded rgba with
'var(--color-bg-overlay)' so the overlay uses the theme token; update the
conditional expression where backgroundColor is assigned in GlobalLoader to use
'var(--color-bg-overlay)' for the non-opaque case.
frontend/src/providers/LoadingProvider.tsx (1)

13-17: Use a real delay threshold for anti-flicker behavior.

On Line 16, setTimeout(..., 0) only defers to the next tick and usually won’t reduce flash on quick loads. Consider a small real threshold (e.g. 120–200ms).

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

In `@frontend/src/providers/LoadingProvider.tsx` around lines 13 - 17, The current
useEffect in LoadingProvider defers showing the loader with setTimeout(..., 0)
which doesn't prevent flicker; change the timeout to a small real threshold
(e.g., 150ms) so the timer created when isLoading becomes true calls
setShowLoader(true) after that delay, while keeping the existing cleanup that
clears the timer; update references in the useEffect where isLoading,
setShowLoader and the timer are used to use the new delay constant (120–200ms,
e.g., 150ms).
frontend/src/components/context/ExtendableContextMenu.tsx (2)

112-112: Simplify redundant submit label expression.

Both branches return the same string.

💡 Suggested fix
-          {isAnyValidating ? 'Submit' : 'Submit'}
+          Submit
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/context/ExtendableContextMenu.tsx` at line 112, In
ExtendableContextMenu.tsx inside the JSX rendering (the Submit label currently
written as the redundant ternary {isAnyValidating ? 'Submit' : 'Submit'}),
replace the ternary expression with the plain string 'Submit' to remove the
unnecessary conditional and simplify the markup.

92-98: Associate labels/errors with inputs for accessibility.

The input label is not programmatically connected, and error text is not announced to assistive tech.

💡 Suggested fix
           if (field.type === 'input') {
+            const inputId = `context-menu-input-${i}`
+            const errorId = `context-menu-error-${i}`
             return (
               <div key={i}>
-                <label>{field.label}</label>
+                <label htmlFor={inputId}>{field.label}</label>
                 <input
+                  id={inputId}
                   placeholder={field.placeholder}
                   value={values[i] ?? ''}
                   onChange={e => handleChange(i, field, e.target.value)}
+                  aria-invalid={Boolean(errors[i])}
+                  aria-describedby={errors[i] ? errorId : undefined}
                 />
-                {errors[i] && <span style={{ color: 'red', fontSize: '12px' }}>{errors[i]}</span>}
+                {errors[i] && (
+                  <span id={errorId} role="alert" style={{ color: 'red', fontSize: '12px' }}>
+                    {errors[i]}
+                  </span>
+                )}
               </div>
             )
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/context/ExtendableContextMenu.tsx` around lines 92 -
98, The label and error text must be programmatically associated with the input
in ExtendableContextMenu.tsx: give each input a unique id (e.g., derived from
field.name or the index i), set the label's htmlFor to that id, add
aria-describedby on the input pointing to the error span id when errors[i]
exists, and set aria-invalid={!!errors[i]} so assistive tech announces
validation; update the span to have a matching id (e.g., `${id}-error`) and keep
handleChange/onChange usage 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 `@frontend/src/App.tsx`:
- Around line 35-57: There are plain text nodes after Route elements inside
Routes (e.g., <Route path="/Login" element={<Login />} />, <Route
path="/Test/Theme" element={<ThemeTest />} />, ProtectedRoutes wrapper and
children like Home, Test, Leagues, League, Trainers, Trainer, Teams, Team,
DocsRoot, DocsHierarchy, DocsLeagues, DocsTrainers) which create invalid JSX
inside <Routes>; replace each trailing text description (e.g., " - login page")
with a JSX comment (/* ... */) so the Routes children contain only valid
elements and comments (e.g., change `... /> - login page` to `... /> {/* login
page */}`).

In `@frontend/src/components/api/DisplayResourceGeneric.tsx`:
- Around line 24-26: Normalize and validate the page query param before use:
when reading searchParams.get('page') in DisplayResourceGeneric, parse it into
an integer and if it is NaN or less than 1 default to 1 (assign to pageNum);
ensure setPageNum also only writes positive integer strings (e.g., coerce input
to Math.max(1, Math.floor(page))). Update the logic around useSearchParams,
pageNum, and setPageNum to enforce this normalization so invalid hand-edited
values like "foo" or "-1" become page 1.
- Around line 43-49: The current items.map callback nests a Link inside a button
which is invalid; change the outer element to be the Link (use Link as the
primary interactive element produced in items.map with key={item.id} and
to={path(item.id)}) and move the onContextMenu handler onto that Link
(onContextMenu={e => onContextMenu?.(e, item)}), or alternatively keep a button
and perform navigation imperatively using history/navigation in the button’s
onClick—do not nest Link inside a button and ensure only a single interactive
element per item (refer to items.map, onContextMenu, Link, path).

In `@frontend/src/components/context/ExtendableContextMenu.tsx`:
- Line 8: The onSubmit prop in ExtendableContextMenu is currently called
fire-and-forget which can cause unhandled rejections and duplicate submissions;
update the component to await the result of onSubmit and prevent concurrent
submits by adding an isSubmitting state flag (e.g., useState<boolean>
isSubmitting) and use it to short-circuit subsequent clicks, wrap the await in
try/catch to handle rejections and a finally to reset isSubmitting, and apply
this pattern to every call-site inside ExtendableContextMenu that invokes
props.onSubmit so submissions are serialized and errors are handled.
- Around line 72-73: The inline style in ExtendableContextMenu sets background:
'white' and border: '1px solid black', which bypasses theming; replace these
hardcoded values with theme tokens (e.g., use the app theme's background and
border/divider tokens via your theme provider or useTheme hook) so the menu uses
theme.palette/background and theme.palette/divider (or your project's equivalent
tokens) instead of 'white'/'black'; update the style object in
ExtendableContextMenu to read theme values for background and border and remove
hardcoded colors.
- Around line 41-47: The async validation block for field.validate can produce
stale/out-of-order results and doesn't handle thrown errors; wrap the await in a
try/catch and gate state updates with a validation token or value snapshot so
only the latest validation result mutates state: when starting validation for
field.validate (use the same index i and current val), generate a unique token
(or capture the current val) and store it locally, call setValidating as now,
then await field.validate inside try/catch, on success check the token/snapshot
still matches the current input before calling setValid and setErrors, on
failure setErrors appropriately (use field.validationError or the caught error
message), and in finally only clear setValidating for that index if the
token/snapshot still matches; reference field.validate, setValidating, setValid,
setErrors and the index i/val to locate where to apply this.

In `@frontend/src/components/error/ErrorBanner.tsx`:
- Around line 22-33: The close button in ErrorBanner (the button with
onClick={clearError}) has no accessible name; add an accessible label such as
aria-label="Close" or a more descriptive label (e.g., aria-label={`Dismiss
error: ${errorMessage}`}) to the button so screen readers get a meaningful
control name; alternatively include visually-hidden text inside the button for
accessibility while keeping the icon-only appearance.

In `@frontend/src/components/modals/CreationModal.tsx`:
- Around line 79-105: The submit handler (handleSubmit) needs an isSubmitting
flag to prevent duplicate requests: add a state variable (e.g., isSubmitting /
setIsSubmitting) and at the top of handleSubmit return early if isSubmitting is
true, then setIsSubmitting(true) just before calling await onSubmit({ tableName,
data: formData }) and ensure setIsSubmitting(false) in a finally block after
onSubmit/onReload/pipeCancel/setFormData to reset the flag on success or error;
update the submit button(s) (the buttons referenced around lines handling the
modal submit at the end of the file) to include disabled={isSubmitting || /*
existing disabled conditions */} so the UI prevents repeated clicks while the
create request is in flight.
- Around line 59-76: The uniqueness validation can race and apply stale results
in handleChange; introduce a ref (e.g., latestValidateValueRef) keyed by field
name to store the latest value/request id when starting validateUnique in
handleChange, then after await check that the stored value matches the current
value (or compare with formData[fieldName]) before calling setErrors or toggling
setIsValidating for that field; ensure you update the ref when new validations
start and only clear/set errors and setIsValidating when the check confirms the
result is for the latest value.

In `@frontend/src/components/ProtectedRoutes.tsx`:
- Around line 14-20: The useEffect in ProtectedRoutes currently calls
startLoading() and stopLoading() directly based on the auth `loading` boolean,
which can clear a global loader started by unrelated flows; update the logic to
use scoped or reference-counted loading operations instead of a single toggle:
when `loading` becomes true call a start method that accepts a unique key or
increments a counter (e.g., startLoading("ProtectedRoutes" or
incrementLoading)), and when `loading` becomes false call the matching stop
method that removes that key or decrements the counter (e.g.,
stopLoading("ProtectedRoutes" or decrementLoading)); modify the global loading
API and the ProtectedRoutes useEffect to use these keyed/incremental operations
so other concurrent loaders are not prematurely cleared.

In `@frontend/src/helpers/leagues/fetchLeaguesIndex.ts`:
- Around line 5-14: The current fetch in fetchLeaguesIndex only handles network
rejections and returns any HTTP error body as success; update the call that
assigns response (the fetch to `${BASE_URL}/categories/index?page=${pageNum}`)
to check response.ok after the await (or in a then) and treat non-OK responses
as errors: call throwError with a message including response.status and
statusText (or response.text()) and return null, so only successful 2xx
responses proceed to await response.json().

In `@frontend/src/helpers/leagues/fetchLeaguesSpecific.ts`:
- Around line 5-14: The current fetch call in fetchLeaguesSpecific.ts only
catches network errors but still calls response.json() for non-OK HTTP
responses; update the logic after the fetch (the response handling around the
response variable) to check response.ok and handle non-2xx statuses explicitly
(e.g., call throwError with a message containing response.status and
response.statusText and return null) before calling response.json(), so API
errors (404/500) don't get parsed as success.

In `@frontend/src/helpers/leagues/renameLeague.ts`:
- Around line 13-27: The renameLeague function is calling the wrong endpoint and
only handling network errors; change the fetch URL from
`/categories/update?id=${id}` to the controller-compatible
`/categories?id=${id}` and strengthen error handling: after the fetch (and still
catching network errors with the existing catch that calls throwError), check
response.ok and call throwError with status and statusText (and response text if
available) for non-2xx responses, then wrap the await response.json() in
try/catch to handle JSON parse errors and call throwError on parse failure;
reference the renameLeague function, the throwError helper, the fetch call,
response.ok check, and response.json() when making these changes.

In `@frontend/src/hooks/load/useResource.ts`:
- Around line 9-29: The load function is using stale values because fetchRef and
pageRef are never updated after mount; update the refs whenever inputs change
and keep load stable: add effects like useEffect(() => { fetchRef.current =
fetchFn }, [fetchFn]) and useEffect(() => { pageRef.current = pageNum },
[pageNum]) so fetchRef.current(pageRef.current) inside load uses the latest
values, leaving load memoized via useCallback as-is to keep the
DisplayResourceGeneric useEffect([load]) behavior intact.

In `@frontend/src/index.css`:
- Around line 1-3: Stylelint is flagging the Tailwind directives (`@tailwind`
base; `@tailwind` components; `@tailwind` utilities;) as unknown at-rules; fix by
updating Stylelint config to allow Tailwind at-rules—either extend a
Tailwind-aware config (e.g., add "stylelint-config-tailwindcss" to extends) or
set the scss rule to ignore these names (e.g., configure
"scss/at-rule-no-unknown": [true, { "ignoreAtRules":
["tailwind","apply","variants","responsive","screen"] }]); ensure the change is
applied so the `@tailwind` directives are no longer reported.

In `@frontend/src/pages/leagues/League.tsx`:
- Around line 19-24: Validate categoryId before calling startLoading, or wrap
the async fetch flow in a try/finally to guarantee stopLoading runs on every
exit; specifically, move or add the categoryId check (used with throwError) so
it executes before startLoading OR call stopLoading() immediately before each
early return, and after awaiting fetchLeaguesSpecific ensure leagueJson is not
null/undefined before using Object.hasOwn (e.g., check if (!leagueJson ||
!Object.hasOwn(leagueJson, 'user_id')) then call stopLoading() and
throwError('No league found')). In short: ensure startLoading/stopLoading are
paired on every path around the fetchLeagueSpecific call in League (functions:
startLoading, stopLoading, throwError, fetchLeaguesSpecific, setLeague).

In `@frontend/src/pages/leagues/Leagues.tsx`:
- Around line 39-42: The renameLeague handler always flips setReloadKey
regardless of API success; change renameLeague to await the result of
renameLeagueApi(contextMenuItem.id, name), store it in a variable, and only call
setReloadKey(k => k * -1) (or otherwise invalidate) when that result is truthy
(i.e., renameLeagueApi returned a non-null success value); keep the early return
when !contextMenuItem and ensure you reference the renameLeague function,
renameLeagueApi call, contextMenuItem, and setReloadKey when making the change.

In `@frontend/src/pages/test/ThemeTest.tsx`:
- Around line 9-15: The dark state is hardcoded to true causing a mismatch;
update the ThemeTest component to initialize dark from the DOM (useState(() =>
!document.documentElement.classList.contains('light'))) and add a useEffect that
watches for changes to document.documentElement.classList (MutationObserver) to
call setDark accordingly; keep the existing toggleTheme (setDark and
document.documentElement.classList.toggle('light')) but ensure the state and DOM
are synchronized on mount and when the html class changes (follow the pattern
used in Pokeball.tsx).

In `@frontend/src/styles/theme.css`:
- Line 6: Change the CSS property value for text-rendering from
"optimizeLegibility" to lowercase "optimizelegibility" so Stylelint passes;
locate the declaration using the text-rendering property in theme.css and
normalize the keyword to optimizelegibility.

---

Outside diff comments:
In `@frontend/src/helpers/manageResource/createNewResource.ts`:
- Around line 17-37: The createNewResource function currently swallows fetch
errors and always calls onReload; update createNewResource so it checks the
fetch response (use response.ok or parsed payload status) and throws or rethrows
on failure instead of just console.logging, and only call await onReload() after
a successful create. Locate the fetch block in createNewResource, ensure non-2xx
responses and caught exceptions are propagated (or return an error) so callers
know the create failed, and keep onReload invocation conditional on a successful
create.

In `@frontend/src/helpers/validation/checkUniqueness.ts`:
- Around line 6-13: The function that fetches
`${BASE_URL}/${tableName}/check-unique` currently returns raw JSON without
checking response status or verifying the shape; update the fetch handling in
checkUniqueness (use the existing response variable) to first verify
response.ok, parse the JSON, and validate that the parsed object contains the
expected boolean field (e.g., unique or isUnique) before returning; if
response.ok is false or the shape is invalid, throw or return a clear
error/false result so callers cannot treat an error object as unique.

---

Nitpick comments:
In `@frontend/src/App.tsx`:
- Line 37: The Test Theme route is currently always registered; wrap the Route
that renders ThemeTest (the JSX line Route path="/Test/Theme"
element={<ThemeTest />} inside App or the component that builds routes) in a
dev-only conditional so it only mounts in development builds (e.g., guard with
process.env.NODE_ENV === 'development' or your build's dev flag like
import.meta.env.DEV). Keep the same Route JSX but render it only when the dev
flag is true so production builds do not expose the /Test/Theme test page.

In `@frontend/src/components/context/ExtendableContextMenu.tsx`:
- Line 112: In ExtendableContextMenu.tsx inside the JSX rendering (the Submit
label currently written as the redundant ternary {isAnyValidating ? 'Submit' :
'Submit'}), replace the ternary expression with the plain string 'Submit' to
remove the unnecessary conditional and simplify the markup.
- Around line 92-98: The label and error text must be programmatically
associated with the input in ExtendableContextMenu.tsx: give each input a unique
id (e.g., derived from field.name or the index i), set the label's htmlFor to
that id, add aria-describedby on the input pointing to the error span id when
errors[i] exists, and set aria-invalid={!!errors[i]} so assistive tech announces
validation; update the span to have a matching id (e.g., `${id}-error`) and keep
handleChange/onChange usage as-is.

In `@frontend/src/components/load/GlobalLoader.tsx`:
- Line 16: Replace the hardcoded overlay color in the GlobalLoader component by
using the theme token: when computing the backgroundColor (the property
currently set to opaque ? 'var(--color-bg)' : 'rgba(0, 0, 0, 0.5)'), swap the
hardcoded rgba with 'var(--color-bg-overlay)' so the overlay uses the theme
token; update the conditional expression where backgroundColor is assigned in
GlobalLoader to use 'var(--color-bg-overlay)' for the non-opaque case.

In `@frontend/src/Main.tsx`:
- Around line 10-14: The module currently accesses window and document at
top-level (prefersDark using window.matchMedia and
document.documentElement.classList.add('light')), which breaks in non-browser
environments; wrap that logic in a runtime guard such as checking typeof window
!== 'undefined' && typeof document !== 'undefined' (and that window.matchMedia
exists) before evaluating prefersDark or mutating document, so the code only
runs in browsers and is safe for SSR/tests.

In `@frontend/src/providers/LoadingProvider.tsx`:
- Around line 13-17: The current useEffect in LoadingProvider defers showing the
loader with setTimeout(..., 0) which doesn't prevent flicker; change the timeout
to a small real threshold (e.g., 150ms) so the timer created when isLoading
becomes true calls setShowLoader(true) after that delay, while keeping the
existing cleanup that clears the timer; update references in the useEffect where
isLoading, setShowLoader and the timer are used to use the new delay constant
(120–200ms, e.g., 150ms).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: caf44dfa-bfef-4c6a-b21e-62af39d6e29f

📥 Commits

Reviewing files that changed from the base of the PR and between c675a3b and dd7c0bd.

⛔ Files ignored due to path filters (3)
  • frontend/package-lock.json is excluded by !**/package-lock.json
  • frontend/public/pokeballDark.svg is excluded by !**/*.svg
  • frontend/public/pokeballLight.svg is excluded by !**/*.svg
📒 Files selected for processing (34)
  • frontend/package.json
  • frontend/postcss.config.js
  • frontend/src/App.tsx
  • frontend/src/Main.tsx
  • frontend/src/components/Pokeball.tsx
  • frontend/src/components/ProtectedRoutes.tsx
  • frontend/src/components/api/DisplayResourceGeneric.tsx
  • frontend/src/components/context/ExtendableContextMenu.tsx
  • frontend/src/components/error/ErrorBanner.tsx
  • frontend/src/components/leagues/LeagueContextMenu.tsx
  • frontend/src/components/load/GlobalLoader.tsx
  • frontend/src/components/load/LoadResourceIndex.tsx
  • frontend/src/components/load/ShowLoader.tsx
  • frontend/src/components/modals/CreationModal.tsx
  • frontend/src/helpers/exports/exportError.ts
  • frontend/src/helpers/exports/exportLoading.ts
  • frontend/src/helpers/leagues/fetchLeaguesIndex.ts
  • frontend/src/helpers/leagues/fetchLeaguesSpecific.ts
  • frontend/src/helpers/leagues/renameLeague.ts
  • frontend/src/helpers/manageResource/createNewResource.ts
  • frontend/src/helpers/validation/checkUniqueness.ts
  • frontend/src/hooks/load/useResource.ts
  • frontend/src/index.css
  • frontend/src/pages/leagues/League.tsx
  • frontend/src/pages/leagues/Leagues.tsx
  • frontend/src/pages/test/Test.tsx
  • frontend/src/pages/test/ThemeTest.tsx
  • frontend/src/providers/ErrorProvider.tsx
  • frontend/src/providers/LoadingProvider.tsx
  • frontend/src/redux/errorSlice.ts
  • frontend/src/redux/loadingSlice.ts
  • frontend/src/redux/store.ts
  • frontend/src/styles/theme.css
  • frontend/tailwind.config.ts
💤 Files with no reviewable changes (1)
  • frontend/src/components/load/LoadResourceIndex.tsx

Comment thread frontend/src/App.tsx Outdated
Comment thread frontend/src/components/api/DisplayResourceGeneric.tsx
Comment thread frontend/src/components/api/DisplayResourceGeneric.tsx Outdated
Comment thread frontend/src/components/context/ExtendableContextMenu.tsx Outdated
Comment thread frontend/src/components/context/ExtendableContextMenu.tsx Outdated
Comment thread frontend/src/index.css
Comment thread frontend/src/pages/leagues/League.tsx Outdated
Comment thread frontend/src/pages/leagues/Leagues.tsx Outdated
Comment thread frontend/src/pages/test/ThemeTest.tsx Outdated
Comment thread frontend/src/styles/theme.css
@Casjb Casjb changed the title Finishing wireframe Add Tailwind CSS setup, theme system, and UI components / flow Apr 8, 2026
Copy link
Copy Markdown

@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: 2

♻️ Duplicate comments (2)
frontend/src/pages/leagues/League.tsx (1)

27-27: ⚠️ Potential issue | 🟠 Major

Validate categoryId as a strict integer before fetch.

parseInt(categoryId) can accept partial values (e.g. "12abc"), which may fetch the wrong league ID. Reject invalid IDs explicitly before calling fetchLeaguesSpecific.

Suggested fix
-      const leagueJson = await fetchLeaguesSpecific(parseInt(categoryId))
+      const id = Number(categoryId)
+      if (!Number.isInteger(id) || id <= 0) {
+        throwError('Invalid league id')
+        return
+      }
+
+      const leagueJson = await fetchLeaguesSpecific(id)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/leagues/League.tsx` at line 27, The code calls
fetchLeaguesSpecific(parseInt(categoryId)) without strict validation, allowing
inputs like "12abc"; update the League component to validate categoryId before
calling fetchLeaguesSpecific by ensuring it matches a strict integer pattern
(e.g. /^\d+$/) or by parsing with Number and verifying Number.isInteger and
string equality to the original, then convert to an integer and call
fetchLeaguesSpecific with that safe value; if validation fails, handle it
explicitly (show error UI, return early, or redirect) so fetchLeaguesSpecific is
only ever called with a proven integer id.
frontend/src/components/context/ExtendableContextMenu.tsx (1)

69-81: ⚠️ Potential issue | 🟠 Major

Handle submit rejections and guard re-entry inside handleSubmit.

void handleSubmit() with no rejection handling can still produce unhandled promise rejections, and there’s no in-function guard against concurrent invocation.

💡 Suggested fix
   const handleSubmit = async () => {
+    if (submitting) return
     setSubmitting(true)
     try {
-      await Promise.all(
+      await Promise.all(
         fields.map((field, i) =>
           field.type === 'input' && values[i] !== undefined
             ? Promise.resolve(field.onSubmit(values[i]))
             : Promise.resolve()
         )
       )
+    } catch {
+      // surface via app-level error handling if available
     } finally {
       setSubmitting(false)
     }
   }

Also applies to: 147-147

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

In `@frontend/src/components/context/ExtendableContextMenu.tsx` around lines 69 -
81, handleSubmit currently can be invoked concurrently and may produce unhandled
rejections; add an in-function re-entry guard and proper rejection handling: at
the top of handleSubmit check and return if submitting is true (use the
submitting state), then setSubmitting(true), await Promise.all over fields.map
calling field.onSubmit(values[i]) only when field.type === 'input' and values[i]
!== undefined, wrap the await in try/catch to handle/log any errors from
field.onSubmit, and finally setSubmitting(false) in a finally block; update both
occurrences of handleSubmit (lines cited) to use this pattern.
🧹 Nitpick comments (2)
frontend/src/pages/leagues/League.tsx (1)

46-46: Set explicit button type for safer reuse.

Add type="button" to avoid accidental form submission if this markup is ever nested inside a <form>.

Suggested fix
-      <button onClick={() => navigate(-1)}>{'<- Leagues'}</button>
+      <button type="button" onClick={() => navigate(-1)}>{'<- Leagues'}</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/leagues/League.tsx` at line 46, The back-navigation button
in the League component is missing an explicit type which can cause accidental
form submission if this component is ever nested in a form; update the JSX
button (the element with onClick={() => navigate(-1)} in the League component)
to include type="button" so it is explicitly a non-submit button.
frontend/src/components/context/ExtendableContextMenu.tsx (1)

133-133: Use a theme token for error color instead of hardcoded 'red'.

This keeps error styling consistent across light/dark themes.

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

In `@frontend/src/components/context/ExtendableContextMenu.tsx` at line 133, The
inline style in ExtendableContextMenu sets color: 'red' which hardcodes error
color; replace it with the app's theme token instead (e.g.
useTheme().colors.error or a CSS variable like 'var(--color-error)') by
retrieving the theme via the project’s theme hook/utility and using that token
for the style prop (or apply a className such as 'text-error'); update the
element that currently has style={{ color: 'red', fontSize: '12px' }} to
reference the theme token so error color respects light/dark themes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/context/ExtendableContextMenu.tsx`:
- Around line 46-66: When validation is skipped because the input is empty (the
condition is false for field.validate && val.length > 0), ensure you explicitly
clear the validating flag for that index so it can't remain true; update the
block around field.validate to add an else branch that calls setValidating(prev
=> ({ ...prev, [i]: false })) (optionally also clear errors via setErrors(prev
=> ({ ...prev, [i]: undefined })) or setValid if desired) referencing the same
index i and the existing state setters (setValidating, setErrors, setValid) and
latestValues.current logic.

In `@frontend/src/pages/leagues/League.tsx`:
- Around line 26-37: The loadLeague code calls fetchLeaguesSpecific but lacks a
catch, so thrown errors bypass the app's global UI error flow; wrap the fetch in
try/catch/finally (or add a catch after the existing try) and in the catch call
throwError(error) (or throwError(String(error)) for safe messaging) before
returning, keeping stopLoading() in the finally block; reference
fetchLeaguesSpecific, loadLeague (or the enclosing function), throwError,
setLeague and stopLoading to locate and update the code.

---

Duplicate comments:
In `@frontend/src/components/context/ExtendableContextMenu.tsx`:
- Around line 69-81: handleSubmit currently can be invoked concurrently and may
produce unhandled rejections; add an in-function re-entry guard and proper
rejection handling: at the top of handleSubmit check and return if submitting is
true (use the submitting state), then setSubmitting(true), await Promise.all
over fields.map calling field.onSubmit(values[i]) only when field.type ===
'input' and values[i] !== undefined, wrap the await in try/catch to handle/log
any errors from field.onSubmit, and finally setSubmitting(false) in a finally
block; update both occurrences of handleSubmit (lines cited) to use this
pattern.

In `@frontend/src/pages/leagues/League.tsx`:
- Line 27: The code calls fetchLeaguesSpecific(parseInt(categoryId)) without
strict validation, allowing inputs like "12abc"; update the League component to
validate categoryId before calling fetchLeaguesSpecific by ensuring it matches a
strict integer pattern (e.g. /^\d+$/) or by parsing with Number and verifying
Number.isInteger and string equality to the original, then convert to an integer
and call fetchLeaguesSpecific with that safe value; if validation fails, handle
it explicitly (show error UI, return early, or redirect) so fetchLeaguesSpecific
is only ever called with a proven integer id.

---

Nitpick comments:
In `@frontend/src/components/context/ExtendableContextMenu.tsx`:
- Line 133: The inline style in ExtendableContextMenu sets color: 'red' which
hardcodes error color; replace it with the app's theme token instead (e.g.
useTheme().colors.error or a CSS variable like 'var(--color-error)') by
retrieving the theme via the project’s theme hook/utility and using that token
for the style prop (or apply a className such as 'text-error'); update the
element that currently has style={{ color: 'red', fontSize: '12px' }} to
reference the theme token so error color respects light/dark themes.

In `@frontend/src/pages/leagues/League.tsx`:
- Line 46: The back-navigation button in the League component is missing an
explicit type which can cause accidental form submission if this component is
ever nested in a form; update the JSX button (the element with onClick={() =>
navigate(-1)} in the League component) to include type="button" so it is
explicitly a non-submit button.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c3324f3-143d-4aec-a380-c5500ef54a19

📥 Commits

Reviewing files that changed from the base of the PR and between dd7c0bd and ac5fa38.

📒 Files selected for processing (11)
  • frontend/fixme.md
  • frontend/src/App.tsx
  • frontend/src/Main.tsx
  • frontend/src/components/api/DisplayResourceGeneric.tsx
  • frontend/src/components/context/ExtendableContextMenu.tsx
  • frontend/src/components/load/GlobalLoader.tsx
  • frontend/src/helpers/manageResource/createNewResource.ts
  • frontend/src/helpers/validation/checkUniqueness.ts
  • frontend/src/pages/leagues/League.tsx
  • frontend/src/pages/leagues/Leagues.tsx
  • frontend/src/pages/test/ThemeTest.tsx
✅ Files skipped from review due to trivial changes (4)
  • frontend/src/Main.tsx
  • frontend/fixme.md
  • frontend/src/components/load/GlobalLoader.tsx
  • frontend/src/pages/test/ThemeTest.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • frontend/src/helpers/validation/checkUniqueness.ts
  • frontend/src/App.tsx
  • frontend/src/helpers/manageResource/createNewResource.ts
  • frontend/src/components/api/DisplayResourceGeneric.tsx
  • frontend/src/pages/leagues/Leagues.tsx

Comment thread frontend/src/components/context/ExtendableContextMenu.tsx
Comment thread frontend/src/pages/leagues/League.tsx
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (2)
frontend/src/components/context/ExtendableContextMenu.tsx (1)

71-88: Consider surfacing submission errors to the user.

When onSubmit fails, the error is logged to console but no feedback is shown to the user. The menu remains open without any indication that submission failed.

Additionally, since Promise.all runs all field submissions in parallel, partial failures may occur (some succeed, some fail). Consider whether this behavior is acceptable for your use cases.

💡 Optional: Add error state for submission failures
 const [errors, setErrors] = useState<Record<number, string>>({})
 const [submitting, setSubmitting] = useState(false)
+const [submitError, setSubmitError] = useState<string | null>(null)
 
 // ...
 
 const handleSubmit = async () => {
   if (submitting) return
 
   setSubmitting(true)
+  setSubmitError(null)
   try {
     await Promise.all(
       fields.map((field, i) =>
         field.type === 'input' && values[i] !== undefined
           ? Promise.resolve(field.onSubmit(values[i]))
           : Promise.resolve()
       )
     )
   } catch (error) {
     console.error(error)
+    setSubmitError('Submission failed. Please try again.')
   } finally {
     setSubmitting(false)
   }
 }

Then render submitError near the submit button.

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

In `@frontend/src/components/context/ExtendableContextMenu.tsx` around lines 71 -
88, handleSubmit currently swallows errors in console and leaves the menu
ambiguous; add a submitError state (e.g., submitError and setSubmitError) and
set it on catch so failures are surfaced to the UI, render that error text near
the submit button in ExtendableContextMenu, and ensure setSubmitting(false)
still runs; also consider replacing Promise.all with Promise.allSettled or
sequential awaits inside handleSubmit (calling field.onSubmit for each
input-type field in fields using values[i]) and treat any rejected result as a
failure that sets submitError so the menu does not silently remain open on
partial/total failure.
frontend/src/pages/leagues/League.tsx (1)

13-13: Component name doesn't match file name or purpose.

The file is League.tsx (singular) and the docstring says "Individual league page," but the exported function is named Leagues (plural). Consider renaming to League for consistency.

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

In `@frontend/src/pages/leagues/League.tsx` at line 13, The exported component is
named Leagues but the file and docstring indicate it represents a single league;
rename the component/function from Leagues to League (and update any
corresponding default export if explicitly named) to match the file purpose and
docstring, and update any imports elsewhere that reference Leagues to import
League instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@frontend/src/components/context/ExtendableContextMenu.tsx`:
- Around line 71-88: handleSubmit currently swallows errors in console and
leaves the menu ambiguous; add a submitError state (e.g., submitError and
setSubmitError) and set it on catch so failures are surfaced to the UI, render
that error text near the submit button in ExtendableContextMenu, and ensure
setSubmitting(false) still runs; also consider replacing Promise.all with
Promise.allSettled or sequential awaits inside handleSubmit (calling
field.onSubmit for each input-type field in fields using values[i]) and treat
any rejected result as a failure that sets submitError so the menu does not
silently remain open on partial/total failure.

In `@frontend/src/pages/leagues/League.tsx`:
- Line 13: The exported component is named Leagues but the file and docstring
indicate it represents a single league; rename the component/function from
Leagues to League (and update any corresponding default export if explicitly
named) to match the file purpose and docstring, and update any imports elsewhere
that reference Leagues to import League instead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc6b6777-8bdb-4e19-bb54-441318f2d723

📥 Commits

Reviewing files that changed from the base of the PR and between ac5fa38 and 1e80807.

📒 Files selected for processing (2)
  • frontend/src/components/context/ExtendableContextMenu.tsx
  • frontend/src/pages/leagues/League.tsx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant