Skip to content

Feature/layout signin signup#81

Merged
kskrenes merged 22 commits into
mainfrom
feature/layout-signin-signup
Jun 1, 2026
Merged

Feature/layout signin signup#81
kskrenes merged 22 commits into
mainfrom
feature/layout-signin-signup

Conversation

@kskrenes

@kskrenes kskrenes commented Jun 1, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Google Sign-in button now auto-resizes, initializes more robustly, and avoids duplicate initialization.
    • Added reusable panel components for headers and error messaging.
    • Inputs support an inline link (e.g., "Forgot password?") and password placeholders.
  • Style

    • Reworked login, signup, and MFA UIs into centered panel layouts with unified error panels and refined input focus/colour tokens.
  • Chores

    • Documentation checklist updated; development typing package added and project typing config adjusted.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dad23dee-069c-4cd7-a575-d14cb5f88672

📥 Commits

Reviewing files that changed from the base of the PR and between 0d125fc and 29868ae.

📒 Files selected for processing (2)
  • docs/nae-todo.txt
  • src/components/google-login-button.tsx
✅ Files skipped from review due to trivial changes (1)
  • docs/nae-todo.txt
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/google-login-button.tsx

📝 Walkthrough

Walkthrough

Modernizes auth UIs: introduces PanelHeader/PanelError, consolidates form state and errors, updates globals/input utilities, refactors Google Identity Services button for guarded inline init and responsive sizing, and adjusts MFA verification layout.

Changes

Auth UI Panel Components and State Refactoring

Layer / File(s) Summary
Documentation, dependencies, and type configuration
docs/nae-todo.txt, package.json, tsconfig.json
Updated TODO checklist wording and completion marks; added @types/google.accounts devDependency and compilerOptions.types entry for google.accounts.
New panel UI components and global theme
src/components/panel-error.tsx, src/components/panel-header.tsx, src/app/globals.css
Added PanelError and PanelHeader; added --panel-error/--panel-error-border tokens for light/dark themes; updated input-base focus-visible outline, input-label typography, added input-link utility, and enhanced input-error with background and focus-visible outline.
Input component link prop and password placeholders
src/components/nae-input.tsx, src/components/nae-set-password.tsx
Input accepts optional link ({label, href}) shown next to the label; password and confirm-password fields now include placeholders.
MFA components refactored to use panel components
src/components/mfa-challenge.tsx, src/components/mfa-verify-form.tsx
MFAChallenge uses PanelHeader for title/description; MFAVerifyForm uses PanelError for validation feedback and adjusts layout/labels.
Google Identity Services button integration refactor
src/components/google-login-button.tsx
Reworked Google button: measures container width with ResizeObserver (clamped), reuses/injects GIS script tag, guards initialize with a ref to avoid double-init under Strict Mode, initializes google.accounts.id with a stable callbackRef that calls backend auth, and renders into gsi-target-btn with width-driven sizing.
Login page state and UI refactoring
src/app/(ui)/login/page.tsx
Simplified login state to email/password/error and awaitingRedirect; handleSubmit validates fields, sets awaitingRedirect, handles MFA branching, and maps axios 401 vs other failures to user-facing messages; replaced ShieldAlert with PanelError and restructured into PanelHeader-based layout.
Signup page state and UI refactoring
src/app/(ui)/signup/page.tsx
Unified signup error to a single string; handleSubmit validates inputs and surfaces API errors via getErrorMessage; replaced inline error with PanelError and updated UI to use PanelHeader and a loader-enabled submit label.

Sequence Diagram(s)

sequenceDiagram
  participant ResizeObserver as ResizeObserver
  participant GoogleLoginButton as GoogleLoginButton
  participant GoogleScript as GIS Script
  participant GoogleAccounts as google.accounts.id
  participant BackendAuth as Backend Auth
  ResizeObserver->>GoogleLoginButton: report containerWidth
  GoogleLoginButton->>GoogleScript: reuse or inject GIS script tag
  GoogleScript-->>GoogleLoginButton: script loaded
  GoogleLoginButton->>GoogleAccounts: id.initialize(callbackRef)
  GoogleAccounts-->>GoogleLoginButton: renderButton into gsi-target-btn (with width)
  GoogleAccounts->>BackendAuth: POST credential token
  BackendAuth-->>GoogleLoginButton: response (mfaRequired / success)
Loading
sequenceDiagram
  participant User
  participant LoginPage
  participant Backend as Backend Auth
  participant GoogleButton as GoogleLoginButton
  User->>LoginPage: submit email/password
  LoginPage->>LoginPage: validate inputs
  LoginPage->>Backend: POST /api/users/login
  Backend-->>LoginPage: mfaRequired or success/error
  LoginPage->>LoginPage: set mfaPending or error
  User->>GoogleButton: click Google sign-in
  GoogleButton->>Backend: POST /api/auth/google (via credential)
  Backend-->>GoogleButton: mfaRequired or success
  GoogleButton->>LoginPage: update state or redirect
Loading

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

"🐰 I hopped in with a header and a light,
Panel errors glowing warm and bright,
Google buttons stretch to fit the view,
Logins, signups, MFA — all anew,
A tiny rabbit cheered: tidy and right!"

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Feature/layout signin signup' is vague and generic, using branch-naming conventions rather than describing the actual changes in the pull request. Use a more descriptive title that clearly summarizes the main changes, such as 'Refactor sign-in and sign-up pages with panel-based layout and improved error handling' or 'Add PanelHeader and PanelError components to auth UI'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 feature/layout-signin-signup

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/components/google-login-button.tsx (1)

59-111: ⚖️ Poor tradeoff

Consider splitting one-time init from width-driven re-render.

Because this effect depends on containerWidth, id.initialize and renderButton re-run on every clamped width change. During a drag-resize the ResizeObserver fires frequently, re-rendering the Google button repeatedly (visible flicker). Consider initializing once and only re-running renderButton when containerWidth changes, and/or debouncing the observer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/google-login-button.tsx` around lines 59 - 111, The effect
currently calls window.google.accounts.id.initialize and renderButton on every
containerWidth change causing flicker; split concerns by initializing the Google
ID client once (move window.google.accounts.id.initialize into a mount-only
effect tied to window.google/accounts/script load and handleBackendAuth) and
create a separate effect that only calls window.google.accounts.id.renderButton
(or a small wrapper like renderGoogleButton) when containerWidth changes,
optionally debouncing the render call from the ResizeObserver; ensure you still
add/remove the script load listener and clean up debounced timers so script,
renderButton, handleBackendAuth, containerWidth and disposed logic are preserved
and not re-run unnecessarily.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/`(ui)/signup/page.tsx:
- Around line 164-174: Replace the hardcoded color utility and duplicate link
styling: in the block rendering the "Already have an account?" paragraph (the
surrounding div / p and the Link component), remove the `text-gray-600` class
and use the theme muted token (e.g., apply the `--foreground-muted` token via
your theme utility or a text color class that maps to that token), and swap the
Link's custom classes (`text-brand hover:text-brand-highlight font-medium
transition-colors`) for the shared `input-link` utility so the link styling is
consistent with globals.css; target the p element and the Link component when
making these changes.

In `@src/app/globals.css`:
- Around line 282-289: input-error currently sets only
focus:outline-error-highlight which doesn't override input-base's
focus-visible:outline-brand; update the `@utility` input-error rule (the
input-error utility) to explicitly set the focus-visible outline classes (e.g.,
include focus-visible:outline-2 and focus-visible:outline-error-highlight) so
keyboard/focus-visible state shows the error outline; ensure you replace or
augment the existing focus:outline-* class in input-error with focus-visible::*
to correctly override input-base's focus-visible styles.

In `@src/components/google-login-button.tsx`:
- Around line 119-122: The dynamic Tailwind arbitrary class using containerWidth
in className on the gsi-target-btn div will never be generated by Tailwind and
must be replaced with a static approach: remove the
`[&>div>iframe]:w-[${containerWidth}px]!` template literal and instead set the
iframe width via an inline style or by directly updating the iframe element’s
style when you know containerWidth; reference the gsi-target-btn element and
apply style.width. Also avoid re-running renderGoogleButton on every
containerWidth change: initialize Google’s button once (call renderGoogleButton
in a useEffect with empty deps or an init flag) and handle size updates
separately inside the ResizeObserver callback (debounced) to only adjust iframe
width, so the init step is not repeated frequently.

---

Nitpick comments:
In `@src/components/google-login-button.tsx`:
- Around line 59-111: The effect currently calls
window.google.accounts.id.initialize and renderButton on every containerWidth
change causing flicker; split concerns by initializing the Google ID client once
(move window.google.accounts.id.initialize into a mount-only effect tied to
window.google/accounts/script load and handleBackendAuth) and create a separate
effect that only calls window.google.accounts.id.renderButton (or a small
wrapper like renderGoogleButton) when containerWidth changes, optionally
debouncing the render call from the ResizeObserver; ensure you still add/remove
the script load listener and clean up debounced timers so script, renderButton,
handleBackendAuth, containerWidth and disposed logic are preserved and not
re-run unnecessarily.
🪄 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: d153a9ee-792b-44e4-b298-874d0a1e913f

📥 Commits

Reviewing files that changed from the base of the PR and between a477cf2 and 217d710.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • docs/nae-todo.txt
  • package.json
  • src/app/(ui)/login/page.tsx
  • src/app/(ui)/signup/page.tsx
  • src/app/globals.css
  • src/components/google-login-button.tsx
  • src/components/mfa-challenge.tsx
  • src/components/mfa-verify-form.tsx
  • src/components/nae-input.tsx
  • src/components/nae-set-password.tsx
  • src/components/panel-error.tsx
  • src/components/panel-header.tsx
  • src/types/global.d.ts
  • tsconfig.json
💤 Files with no reviewable changes (1)
  • src/types/global.d.ts

Comment thread src/app/(ui)/signup/page.tsx
Comment thread src/app/globals.css
Comment thread src/components/google-login-button.tsx Outdated
@kskrenes

kskrenes commented Jun 1, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai - Can you propose a fix for src/components/google-login-button.tsx that would initialize just once, and only re-run renderButton when containerWidth changes? Would a solution with debouncing work and still allow the button to remain sufficiently responsive? Could we also address the double instantiation warning from the GSI_LOGGER caused by React Strict Mode at the same time, or is that an issue that should be tackled separately?

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat src/components/google-login-button.tsx

Length of output: 4287



@kskrenes — great set of questions. Here's the analysis and a proposed refactor:


Root Causes

Double initialize on resize: The current single effect has [handleBackendAuth, containerWidth] as deps, so both initialize and renderButton re-run on every resize tick. Only renderButton needs to re-run then.

React Strict Mode double-init: Strict Mode does mount → unmount → remount in dev. The disposed flag prevents execution after cleanup, but the second mount starts fresh and calls initialize again. GSI_LOGGER sees two initialize calls and warns.

handleBackendAuth reference churn: Because useCallback has many deps (router, callback, etc.), its reference can change and retrigger the whole effect (including initialize).


Proposed Fix

Three targeted changes:

  1. Module-level gsiInitialized flag — module scope survives Strict Mode remounts, so the second mount sees the flag set and skips initialize.
  2. Split into three effects — script injection (once), init (once, guarded), renderButton (on debounced width).
  3. handleBackendAuthRef — store the callback in a ref so the GSI credential callback always calls the latest version without needing a fresh initialize.
  4. 150 ms debounce on containerWidth — imperceptible to the user, prevents iframe rebuilds on every intermediate ResizeObserver tick.
'use client';

import { AuthLoginResponse, useAuth } from '`@/context-providers/auth-context-provider`';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useRef, useState } from 'react';

interface GoogleLoginButtonProps {
  redirect?: boolean;
  disabled?: boolean;
  callback?: (res: AuthLoginResponse) => void;
  onLoginAttempt?: () => void;
  onLoginError?: () => void;
}

// Module-level guard: survives React Strict Mode's unmount→remount cycle.
// GSI's initialize() must only be called once per page load.
let gsiInitialized = false;

const GSI_SCRIPT_SRC = 'https://accounts.google.com/gsi/client';

export default function GoogleLoginButton({
  redirect = false,
  disabled = false,
  callback,
  onLoginAttempt,
  onLoginError,
}: GoogleLoginButtonProps) {
  const { loggingIn, loginViaGoogle } = useAuth();
  const router = useRouter();

  const containerRef = useRef<HTMLDivElement>(null);
  const [containerWidth, setContainerWidth] = useState<number>(384);
  const [debouncedWidth, setDebouncedWidth] = useState<number>(384);
  const [gsiReady, setGsiReady] = useState(false);

  // Keep a stable ref to handleBackendAuth so the GSI credential callback
  // always invokes the latest version without requiring re-initialization.
  const handleBackendAuth = useCallback(async (token: string) => {
    if (onLoginAttempt) onLoginAttempt();
    try {
      const res = await loginViaGoogle(token);
      if (callback) callback(res);
      if (redirect && !res.data.mfaRequired) {
        router.replace('/dashboard');
      }
    } catch {
      console.error('Error logging in via Google');
      if (onLoginError) onLoginError();
    }
  }, [loginViaGoogle, callback, onLoginAttempt, onLoginError, redirect, router]);

  const handleBackendAuthRef = useRef(handleBackendAuth);
  useEffect(() => { handleBackendAuthRef.current = handleBackendAuth; }, [handleBackendAuth]);

  // Effect 1 — Track container width via ResizeObserver (unchanged).
  useEffect(() => {
    if (!containerRef.current) return;
    const observer = new ResizeObserver((entries) => {
      for (const entry of entries) {
        const width = Math.min(Math.max(entry.contentRect.width, 240), 400);
        setContainerWidth(width);
      }
    });
    observer.observe(containerRef.current);
    return () => observer.disconnect();
  }, []);

  // Effect 2 — Debounce containerWidth (150 ms).
  // Prevents renderButton from firing on every intermediate resize tick.
  useEffect(() => {
    const timer = setTimeout(() => setDebouncedWidth(containerWidth), 150);
    return () => clearTimeout(timer);
  }, [containerWidth]);

  // Effect 3 — Inject the GSI script once (idempotent check).
  useEffect(() => {
    if (document.querySelector(`script[src="${GSI_SCRIPT_SRC}"]`)) return;
    const script = document.createElement('script');
    script.src = GSI_SCRIPT_SRC;
    script.async = true;
    script.defer = true;
    document.body.appendChild(script);
  }, []);

  // Effect 4 — Initialize GSI exactly once, even under React Strict Mode.
  // Uses the module-level `gsiInitialized` flag; does NOT reset on cleanup
  // so Strict Mode's second mount finds it already set and skips the call.
  useEffect(() => {
    const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID;
    if (!clientId) return;

    const doInit = () => {
      if (gsiInitialized || !window.google?.accounts) return;
      gsiInitialized = true;
      window.google.accounts.id.initialize({
        client_id: clientId,
        callback: (response: google.accounts.id.CredentialResponse) => {
          if (response.credential) {
            // Always calls the latest handler via ref — no re-init needed.
            handleBackendAuthRef.current(response.credential);
          }
        },
      });
      setGsiReady(true);
    };

    const script = document.querySelector(`script[src="${GSI_SCRIPT_SRC}"]`) as HTMLScriptElement | null;
    if (window.google?.accounts) {
      doInit();
    } else {
      script?.addEventListener('load', doInit);
      return () => script?.removeEventListener('load', doInit);
    }
  }, []); // Intentionally empty — must only run once

  // Effect 5 — Re-render the button only when width changes (or GSI becomes ready).
  // initialize() is never called here.
  useEffect(() => {
    if (!gsiReady || !window.google?.accounts) return;
    const targetDiv = document.getElementById('gsi-target-btn');
    if (!targetDiv) return;
    window.google.accounts.id.renderButton(targetDiv, {
      type: 'standard',
      theme: 'outline',
      size: 'large',
      text: 'signin_with',
      logo_alignment: 'center',
      width: Math.floor(debouncedWidth),
    });
  }, [gsiReady, debouncedWidth]);

  return (
    <div
      ref={containerRef}
      className='relative w-full max-w-sm mx-auto flex justify-center'
    >
      <div
        id="gsi-target-btn"
        className={`w-full flex justify-center [&>div]:w-full! [&>div>iframe]:w-[${debouncedWidth}px]!`}
      ></div>

      {(loggingIn || disabled) && (
        <div className='absolute top-0 left-0 w-full h-full bg-page/80 cursor-not-allowed'></div>
      )}
    </div>
  );
}

Is the Strict Mode warning worth fixing separately?

No — the module-level gsiInitialized flag directly resolves it as part of the same refactor without any extra complexity. The fix is self-contained: the flag doesn't affect production behavior (the component is never truly unmounted/remounted there), and in dev it simply tells the second Strict Mode mount "already done."

One caveat to note: the module-level flag means if GoogleLoginButton is ever rendered in two separate subtrees simultaneously on the same page, only the first instance calls initialize. The GSI credential callback will route to whichever instance initialized. In practice this is fine since a page would typically only have one active login button, but it's worth a comment in the code.

Debounce responsiveness

150 ms is the sweet spot here — a typical window resize event fires at ~60 fps (≈17 ms intervals), so without debouncing you'd rebuild the iframe dozens of times during a drag. At 150 ms the button resizes about 100–150 ms after the user stops dragging, which is imperceptible during the interaction itself.

  • Replace the contents of src/components/google-login-button.tsx with the refactored version above

The requested changes have been implemented and a pull request has been created: View PR

@kskrenes kskrenes merged commit 2a1c77f into main Jun 1, 2026
1 check passed
@kskrenes kskrenes deleted the feature/layout-signin-signup branch June 1, 2026 22:43
@coderabbitai coderabbitai Bot mentioned this pull request Jun 5, 2026
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