Skip to content

Full UI/UX Refactor with Tailwind CSS and Lucide React#1

Merged
averyfreeman merged 6 commits intomasterfrom
refactor/ui-tailwind-16572977317419127035
Apr 6, 2026
Merged

Full UI/UX Refactor with Tailwind CSS and Lucide React#1
averyfreeman merged 6 commits intomasterfrom
refactor/ui-tailwind-16572977317419127035

Conversation

@averyfreeman
Copy link
Copy Markdown
Owner

@averyfreeman averyfreeman commented Apr 4, 2026

This PR provides a comprehensive refactor of the PleaseVote application, focusing on UI/UX modernization and code simplification.

Key changes:

  • Tailwind CSS Migration: All Bootstrap classes and components have been replaced with a custom-themed Tailwind CSS implementation.
  • Modern Icons: Switched to Lucide React for a more consistent and modern icon set.
  • Election Countdown: Updated the target date to 2026-11-03 and simplified the underlying logic in lib/timerLogic.js.
  • Backend Cleanup: Removed redundant boilerplate from Next.js 9 API routes to improve maintainability.
  • Accessibility: Added ARIA roles and labels to ensure the site is screen-reader friendly and keyboard-navigable.
  • Data Integrity: Added robust error handling for localStorage access to prevent runtime crashes during hydration.
  • Environment Stability: Maintained Node 14 and Next.js 9 constraints while ensuring the package-lock.json remains compatible with npm 6.

All changes have been verified via a successful production build and manual UI inspection with Playwright.


PR created automatically by Jules for task 16572977317419127035 started by @averyfreeman

Summary by Sourcery

Refactor the PleaseVote UI to use Tailwind CSS and Lucide icons, modernizing layouts and interactions while simplifying state and data handling.

New Features:

  • Introduce Tailwind-based responsive layouts for home and voter info pages, countdown timer, cards, dialogs, and modals.
  • Add Lucide-based iconography across navigation, alerts, tables, and call-to-action components.
  • Add custom Tailwind theme with One Half-inspired color palette and display fonts.

Bug Fixes:

  • Prevent crashes from malformed or missing localStorage values when reading stored address and election id.
  • Ensure API routes validate query parameters and handle Google Civic API errors with proper HTTP responses.

Enhancements:

  • Rework election, referendum, polling location, and stored-info views into accessible, keyboard-focusable sections with improved empty, loading, and error states.
  • Simplify countdown timer logic and interval handling using a single timeRemaining helper and React-friendly updates.
  • Replace Bootstrap, React-Bootstrap, and styled-components-based layouts with lighter-weight, component-level styling using Tailwind classes.
  • Redesign address and election selection flows with full-screen modals and clearer guidance for users.
  • Improve service disclaimer content structure and styling for readability and external-link clarity.

Build:

  • Remove Bootstrap, React-Bootstrap, and styled-components dependencies and wire up Tailwind CSS with PostCSS and Autoprefixer in Next.js 9.
  • Add Tailwind configuration, global stylesheet, and PostCSS config while keeping Node 14 / npm 6 compatible lockfile.

- Replace Bootstrap and Styled-Components with Tailwind CSS (v1.9.6)
- Implement OneHalfDark color palette and Righteous typography
- Replace icon library with Lucide React
- Update and simplify countdown logic for 2026 Midterm Elections
- Streamline API routes and remove 2020-era boilerplate
- Add ARIA tags and improve keyboard navigation/A11y
- Implement safety checks for localStorage data retrieval
- Modernize favicon.ico with favicon.png
- Clean up package.json dependencies and ensure npm 6 compatibility (lockfile v1)

Co-authored-by: averyfreeman <1308847+averyfreeman@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pleasevote Error Error Apr 5, 2026 11:39am

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Apr 4, 2026

Reviewer's Guide

Refactors the PleaseVote UI from Bootstrap/styled-components to Tailwind + Lucide, modernizes core pages and cards, adds safer localStorage handling, simplifies the countdown timer logic, and hardens the Next.js API routes while preserving the existing Node 14 / Next 9 stack.

Sequence diagram for voter info fetch with robust localStorage and SWR

sequenceDiagram
  actor User
  participant Browser
  participant PrimaryElections
  participant LocalStorage
  participant SWR
  participant ApiVoterInfo
  participant GoogleCivicAPI

  User->>Browser: Open /voterinfo page
  Browser->>PrimaryElections: Render component

  PrimaryElections->>LocalStorage: getItem address
  LocalStorage-->>PrimaryElections: addressString
  PrimaryElections->>LocalStorage: getItem id
  LocalStorage-->>PrimaryElections: idString
  PrimaryElections->>PrimaryElections: try JSON.parse values
  PrimaryElections-->>PrimaryElections: params address,id set or fallback to raw strings

  PrimaryElections->>PrimaryElections: Build url or null
  alt Missing id or address
    PrimaryElections-->>Browser: Return null (no UI rendered)
  else id and address present
    PrimaryElections->>SWR: useSWR url, fetcher
    SWR->>ApiVoterInfo: GET /api/voterInfo?id=id&address=address
    ApiVoterInfo->>ApiVoterInfo: Validate query parameters
    ApiVoterInfo->>GoogleCivicAPI: civic.elections.voterInfoQuery
    GoogleCivicAPI-->>ApiVoterInfo: info or error
    alt Successful response
      ApiVoterInfo-->>SWR: 200 info json
      SWR-->>PrimaryElections: data
      PrimaryElections-->>Browser: Render Tailwind table of primary contests
    else Error from Google Civic API
      ApiVoterInfo-->>SWR: 500 error json
      SWR-->>PrimaryElections: error
      PrimaryElections-->>Browser: Render Lucide AlertCircle error banner
    end
  end
Loading

Sequence diagram for election selection and navigation via modal

sequenceDiagram
  actor User
  participant Browser
  participant HomePage
  participant ElectionInfoModal
  participant ElectionInfo
  participant LocalStorage
  participant ApiElections
  participant Router

  User->>Browser: Open /
  Browser->>HomePage: Render Home component
  HomePage->>HomePage: useEffect read address,id from localStorage
  HomePage-->>Browser: Show Tailwind hero, buttons

  User->>HomePage: Click Choose Election button
  HomePage->>ElectionInfoModal: show = true
  ElectionInfoModal->>ElectionInfo: Mount dialog

  ElectionInfo->>ApiElections: GET /api/elections via SWR
  ApiElections->>GoogleCivicAPI: civic.elections.electionQuery
  GoogleCivicAPI-->>ApiElections: info or error
  alt Success
    ApiElections-->>ElectionInfo: 200 elections json
    ElectionInfo-->>Browser: Render election table with clickable ids
  else Error
    ApiElections-->>ElectionInfo: 500 error json
    ElectionInfo-->>Browser: Show Lucide AlertCircle error banner
  end

  User->>ElectionInfo: Click election id button
  ElectionInfo->>LocalStorage: removeItem id
  ElectionInfo->>LocalStorage: setItem id JSON.stringify selectedId
  ElectionInfo->>Router: router.push /voterinfo
  Router-->>Browser: Navigate to /voterinfo page
Loading

Flow diagram for simplified countdown timer logic using timeRemaining

flowchart TD
  A[CountdownTimer_mounts] --> B[Initialize_state
 timeLeft = timeRemaining endTime]
  B --> C[useEffect_sets_interval
1_second]
  C --> D[Every_second:
call_timeRemaining endTime]
  D --> E{total_ms <= 0?}
  E -->|Yes| F[timeRemaining_returns
zeros_and_error_message]
  E -->|No| G[timeRemaining_returns
computed_days_hours_minutes_seconds]
  F --> H[Update_state_timeLeft
with_zero_values]
  G --> H[Update_state_timeLeft
with_new_values]
  H --> I[Render_label_and_values
in_tailwind_timer_UI]
  C --> J[Cleanup_on_unmount:
clearInterval]
Loading

File-Level Changes

Change Details Files
Migrate layout, cards, dialogs, and modals from React-Bootstrap/styled-components to Tailwind CSS with Lucide icons and new visual design.
  • Replace Bootstrap Jumbotron/buttons/cards/Accordion with Tailwind-based sections and buttons on home and voter info pages.
  • Refactor Cards (CountdownTimer, VoterRegCard, StoredInfoCard, PrimaryElections, GeneralElections, Referendums, PollingLocations) to Tailwind layouts and Lucide icons, including ARIA-friendly toggle buttons.
  • Replace styled-components-based IconsBar with a fixed Tailwind footer using Lucide social icons and dynamic current year display.
  • Update ServiceDisclaimer, AddressDialog, and input Form to new Tailwind styling and Lucide informational icons.
pages/index.jsx
pages/voterinfo.jsx
Components/Cards/CountdownTimer.jsx
Components/Cards/VoterRegCard.jsx
Components/Cards/StoredInfoCard.jsx
Components/Cards/PrimaryElections.jsx
Components/Cards/GeneralElections.jsx
Components/Cards/Referendums.jsx
Components/Cards/PollingLocations.jsx
Components/NavElements/IconsBar.jsx
Components/ServiceDisclaimer.jsx
Components/Dialogs/AddressDialog.jsx
Components/Input/Form.jsx
Improve robustness of client-side state by safely reading from localStorage and gating SWR fetches on available parameters.
  • Wrap localStorage parsing for address/id in try/catch with JSON fallback logic across election cards and modals.
  • Introduce local React state objects (e.g., params, storedData, id) and skip rendering/fetching when required values are missing.
  • Show stored-values CTA differently depending on presence of saved address/id.
Components/Cards/PrimaryElections.jsx
Components/Cards/GeneralElections.jsx
Components/Cards/Referendums.jsx
Components/Cards/PollingLocations.jsx
Components/Cards/StoredInfoCard.jsx
Components/Dialogs/ElectionDialog.jsx
pages/index.jsx
Simplify and harden countdown timer logic while updating the target election date.
  • Rewrite timeRemaining() to return a structured {total, remaining, error} object and handle non-positive durations explicitly.
  • Deprecate ticker() and move interval handling into CountdownTimer via useEffect + setInterval.
  • Update CountdownTimer UI to Tailwind and change end date/labels to use the 2026-11-03 midterm elections.
lib/timerLogic.js
Components/Cards/CountdownTimer.jsx
pages/index.jsx
pages/voterinfo.jsx
Improve Next.js API routes for elections and voter info with parameter validation and error handling.
  • Refactor /api/voterInfo to use the full req object, validate required query params, and wrap civic.elections.voterInfoQuery in try/catch with proper 4xx/5xx responses.
  • Refactor /api/elections to remove module-level state, call civic.elections.electionQuery inside the handler, and add 404 and 500 branches with logged errors.
pages/api/voterInfo.js
pages/api/elections.js
Introduce Tailwind CSS build pipeline and remove Bootstrap/react-bootstrap/styled-components from the app.
  • Swap global CSS imports in _app.js from Bootstrap and custom modal styles to Tailwind globals.
  • Add Tailwind, PostCSS, and Autoprefixer dev dependencies and create minimal Tailwind and PostCSS configs tuned to the existing component paths.
  • Extend Tailwind theme with custom onehalf color palette and fonts used throughout the new UI.
  • Remove Bootstrap, react-bootstrap, and styled-components from package.json (and associated usage in components).
pages/_app.js
package.json
tailwind.config.js
postcss.config.js
styles/globals.css

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • Several components (PrimaryElections, GeneralElections, Referendums, PollingLocations) assume data.data.* is always present; if the API returns an error object (e.g., 4xx/5xx with a different shape), these will throw—consider guarding with optional chaining or explicit checks (e.g., if (!data?.data?.contests) ...) before accessing nested fields.
  • The logic for reading and parsing localStorage values for address and id is duplicated across multiple components; extracting this into a small shared hook (e.g., useStoredAddressAndElectionId) would reduce repetition and keep the parsing/try-catch behavior consistent.
  • The updated timeRemaining now returns an error field when the deadline has passed, but CountdownTimer ignores that and always renders the numeric countdown; if you intend to show a different UI once time is up, consider checking timeLeft.error and rendering a distinct expired state.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Several components (PrimaryElections, GeneralElections, Referendums, PollingLocations) assume `data.data.*` is always present; if the API returns an error object (e.g., 4xx/5xx with a different shape), these will throw—consider guarding with optional chaining or explicit checks (e.g., `if (!data?.data?.contests) ...`) before accessing nested fields.
- The logic for reading and parsing `localStorage` values for `address` and `id` is duplicated across multiple components; extracting this into a small shared hook (e.g., `useStoredAddressAndElectionId`) would reduce repetition and keep the parsing/try-catch behavior consistent.
- The updated `timeRemaining` now returns an `error` field when the deadline has passed, but `CountdownTimer` ignores that and always renders the numeric countdown; if you intend to show a different UI once time is up, consider checking `timeLeft.error` and rendering a distinct expired state.

## Individual Comments

### Comment 1
<location path="Components/Cards/PollingLocations.jsx" line_range="56-57" />
<code_context>
+      </div>
     );

-  if (
-    (data && (data !== null) & !data.data.contests) ||
-    data.data.contests.length <= 0 ||
</code_context>
<issue_to_address>
**issue (bug_risk):** Bitwise `&` is used instead of logical `&&` in the pollingLocations guard.

This condition mixes logical and bitwise operators: `(data && (data !== null) & !data.data.pollingLocations)`. Bitwise `&` coerces operands to numbers and can change the control flow in unexpected ways. Please replace it with logical operators only, e.g.:

```js
if (
  (data && data !== null && !data.data.pollingLocations) ||
  data.data.pollingLocations?.length <= 0 ||
  data.data.pollingLocations === null
) {
  // ...
}
```
</issue_to_address>

### Comment 2
<location path="Components/Cards/PrimaryElections.jsx" line_range="32" />
<code_context>
+  }, []);
+
+  const url = params.address && params.id ? `/api/voterInfo?id=${params.id}&address=${params.address}` : null;
   const fetcher = (url) => fetch(url).then((r) => r.json());
   const { data, error } = useSWR(url, fetcher);

</code_context>
<issue_to_address>
**suggestion (bug_risk):** The SWR fetcher ignores non-2xx HTTP statuses, which can surface as silent data issues.

Because `fetcher` always calls `r.json()`, SWR treats 4xx/5xx responses as successful and never triggers the `error` state. It would be safer to throw when `!r.ok` so errors propagate correctly, for example:

```js
const fetcher = async (url) => {
  const r = await fetch(url);
  if (!r.ok) {
    const error = new Error(`Request failed with status ${r.status}`);
    error.status = r.status;
    error.info = await r.json().catch(() => null);
    throw error;
  }
  return r.json();
};
```

Given this pattern appears in multiple components, consider centralizing the fetcher for reuse and consistent error handling.

Suggested implementation:

```javascript
  const url = params.address && params.id ? `/api/voterInfo?id=${params.id}&address=${params.address}` : null;
  const fetcher = async (url) => {
    const r = await fetch(url);

    if (!r.ok) {
      const error = new Error(`Request failed with status ${r.status}`);
      error.status = r.status;
      error.info = await r.json().catch(() => null);
      throw error;
    }

    return r.json();
  };
   const { data, error } = useSWR(url, fetcher);

```

1. To fully apply your suggestion about centralizing the fetcher, create a shared utility (e.g. `lib/fetcher.js` or `utils/fetcher.js`) exporting this `fetcher` function.
2. Update other components using `useSWR` with inline fetchers to import and use the shared `fetcher` for consistent error handling across the app.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- Replaced Bootstrap and Styled-Components with Tailwind CSS (v1.9.6).
- Implemented OneHalfDark palette and Righteous/Space Mono typography.
- Migrated icon library from FontAwesome/React-Bootstrap to Lucide React.
- Generated and linked a complete suite of 21 icons/favicons from updated source.
- Simplified countdown timer logic, updated for the 2026-11-03 election.
- Refactored backend API routes for better readability and modern error handling.
- Implemented Accessibility (A11y) with ARIA tags and keyboard navigation support.
- Upgraded target environment to Node 20 LTS with OpenSSL legacy support.
- Standardized jsconfig.json for absolute import resolution.
- Cleaned up repository by moving developer scripts to /scripts and removing logs.

Co-authored-by: averyfreeman <1308847+averyfreeman@users.noreply.github.com>
- Performed a complete clean-slate rebuild using React Router v7 (Framework Mode).
- Switched runtime and package manager to Bun.
- Migrated build pipeline to Vite 8.
- Implemented modern UI/UX with Tailwind CSS v4 and Lucide React.
- Integrated Google Civic Information API for real-time voter and election data.
- Leveraged React 19 Server Components for data fetching.
- Added scenario-based testing with Playwright.
- Documented environment changes in ENV_CHANGES.md.

Co-authored-by: averyfreeman <1308847+averyfreeman@users.noreply.github.com>
- Installed @vercel/react-router and configured react-router.config.ts with SSR and Vercel presets.
- Added vercel.json with bunVersion: "1.x" for Vercel's Bun runtime support.
- Updated package.json scripts to use the --bun flag for build and dev.
- Updated .gitignore to exclude build/ and .react-router/ directories.
- Implemented radius-based filtering logic for Polling Locations in voterinfo.tsx.
- Added a Search Radius adjustment UI to the Voter Info page.
- Cleaned up legacy server/ and vercel/ artifacts.

Co-authored-by: averyfreeman <1308847+averyfreeman@users.noreply.github.com>
- Confirmed Vercel Preset in react-router.config.ts with SSR enabled.
- Verified vercel.json with bunVersion: "1.x" for the serverless Bun runtime.
- Ensured all package scripts use the --bun flag for correct environment execution.
- Maintained .gitignore to exclude build and .react-router artifacts.
- Finalized radius-based filtering logic and UI for polling locations.

Co-authored-by: averyfreeman <1308847+averyfreeman@users.noreply.github.com>
@averyfreeman averyfreeman merged commit affb1a3 into master Apr 6, 2026
2 of 3 checks passed
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