Skip to content

feat(auth): in-app auth modal flow#100

Merged
remcostoeten merged 1 commit intodaddyfrom
feature/auth-ui-modal-pr
Feb 14, 2026
Merged

feat(auth): in-app auth modal flow#100
remcostoeten merged 1 commit intodaddyfrom
feature/auth-ui-modal-pr

Conversation

@remcostoeten
Copy link
Owner

@remcostoeten remcostoeten commented Feb 14, 2026

Summary\n- replace standalone /login page with in-app auth modal trigger flow\n- add auth modal context/provider and overlay components\n- update user menu sign-in action to open modal\n- redirect /login to /?auth=1 via proxy and consume query param in provider\n\n## Files\n- apps/web/app/(auth)/login/page.tsx\n- apps/web/app/providers.tsx\n- apps/web/components/auth/user-menu.tsx\n- apps/web/proxy.ts\n- apps/web/components/auth/auth-modal-context.tsx\n- apps/web/components/auth/auth-modal.tsx\n- apps/web/components/auth/auth-overlay.tsx\n- apps/web/components/auth/login-form.tsx\n\n## Notes\n- PR intentionally scoped to auth modal/login UX files only.

Summary by Sourcery

Introduce an in-app authentication modal flow and route redirection to replace the standalone /login page.

New Features:

  • Add a global auth modal provider and overlay for handling sign-in and registration as an in-app experience.
  • Add a reusable login form supporting email/password sign-in, registration, and social providers (GitHub and Google).

Enhancements:

  • Update the application provider tree to include the auth modal and its context globally.
  • Change the user menu sign-in action to open the auth modal instead of navigating to a separate login page.
  • Redirect legacy /login route requests to the home page with a query flag that triggers the auth modal.

Summary by CodeRabbit

  • New Features
    • Replaced page-based login with a modal authentication interface accessible from anywhere in the app.
    • Added social login options via GitHub and Google.
    • Implemented email/password authentication with registration mode support.
    • Mobile-optimized authentication drawer with drag-to-close functionality.
    • Email domain autocomplete suggestions for streamlined credential entry.

@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel
Copy link
Contributor

vercel bot commented Feb 14, 2026

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

Project Deployment Actions Updated (UTC)
skriuw Ready Ready Preview, Comment Feb 14, 2026 3:33pm

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 14, 2026

Reviewer's Guide

Implements a new in-app authentication modal flow that replaces the standalone /login page, wires it into global providers and routing, and updates the user menu to trigger the modal, including a new reusable login form and overlay components.

Sequence diagram for the new in-app auth modal flow including /login redirect

sequenceDiagram
    participant Browser
    participant Proxy
    participant NextRouter
    participant AuthModalProvider
    participant AuthModal
    participant AuthOverlay
    participant LoginForm
    participant AuthClient

    Browser->>Proxy: GET /login
    Proxy-->>Browser: 302 Redirect to /?auth=1

    Browser->>AuthModalProvider: Render with URL /?auth=1
    AuthModalProvider->>AuthModalProvider: read searchParams auth
    AuthModalProvider-->>AuthModalProvider: showAuthModal = true

    AuthModalProvider->>AuthModal: render AuthModal
    AuthModal->>AuthOverlay: isOpen = true allowClose = !requireAuth

    Browser->>AuthOverlay: User interacts (open drawer)
    AuthOverlay->>LoginForm: render LoginForm

    Browser->>LoginForm: Submit credentials
    LoginForm->>AuthClient: signIn or signUp
    AuthClient-->>LoginForm: success callback
    LoginForm-->>AuthOverlay: onSuccess
    AuthOverlay-->>AuthModalProvider: setShowAuthModal false
    AuthModalProvider-->>AuthModal: showAuthModal = false
    AuthModal-->>AuthOverlay: isOpen = false
Loading

Updated class diagram for auth modal, context, overlay, and user menu

classDiagram
    class AuthModalProvider {
      +boolean showAuthModal
      +boolean requireAuth
      +setShowAuthModal(show)
      +setRequireAuth(requireAuth)
      +openAuthModal(options)
    }

    class AuthModalContextValue {
      +boolean showAuthModal
      +function setShowAuthModal(show)
      +boolean requireAuth
      +function setRequireAuth(requireAuth)
      +function openAuthModal(options)
    }

    class useAuthModal {
      +AuthModalContextValue return
    }

    class AuthModal {
      +render()
    }

    class AuthOverlay {
      +boolean isOpen
      +onClose()
      +onSuccess()
      +boolean allowClose
      +render()
    }

    class LoginForm {
      +string title
      +string subtitle
      +onSuccess()
      +render()
      -handleEmailChange(event)
      -handleEmailKeyDown(event)
      -handleGithubSignIn()
      -handleGoogleSignIn()
      -handleSubmit(event)
    }

    class UserMenu {
      +render()
    }

    class AuthButton {
      +children
      +onClick()
      +boolean isLoading
      +boolean disabled
      +string variant
      +string type
      +style
      +icon
      +render()
    }

    class ErrorText {
      +string error
      +render()
    }

    class signIn {
      +social(config, callbacks)
      +email(config, callbacks)
    }

    class signUp {
      +email(config, callbacks)
    }

    class Proxy {
      +proxy(request)
    }

    AuthModalProvider --> AuthModalContextValue : provides
    useAuthModal --> AuthModalContextValue : returns
    AuthModal --> AuthOverlay
    AuthModal --> AuthModalProvider : uses context
    AuthOverlay --> LoginForm
    AuthOverlay --> AuthModalProvider : setShowAuthModal
    LoginForm --> signIn : calls
    LoginForm --> signUp : calls
    UserMenu --> useAuthModal : uses
    UserMenu --> AuthModalProvider : openAuthModal
    Proxy --> AuthModalProvider : sets auth query param
Loading

File-Level Changes

Change Details Files
Replace the standalone /login page with a no-op page and route-level redirect into the in-app auth modal flow.
  • Remove existing full-page AuthPage UI and implementation
  • Return null from the login page component to avoid rendering a standalone layout
  • Add a proxy redirect from /login to / with an auth=1 query parameter to trigger the modal
apps/web/app/(auth)/login/page.tsx
apps/web/proxy.ts
Introduce an AuthModal context/provider and modal overlay hooked into the app provider tree to control auth UI globally.
  • Create AuthModalContext and hook to manage showAuthModal, requireAuth, and an openAuthModal helper, including reading and consuming ?auth=1 from the URL
  • Add an AuthModal component that binds context state into the AuthOverlay UI with allowClose depending on requireAuth
  • Register AuthModalProvider and AuthModal in the root Providers tree so the modal is available app-wide
apps/web/components/auth/auth-modal-context.tsx
apps/web/components/auth/auth-modal.tsx
apps/web/app/providers.tsx
Add a responsive AuthOverlay component implementing the in-app auth drawer/modal UX.
  • Implement a full-screen overlay with blurred backdrop and bottom drawer layout using framer-motion
  • Support mobile drag-to-close gestures and ESC/backdrop close when allowed, while disabling close when requireAuth is true
  • Integrate the LoginForm into the overlay and wire onSuccess/onClose behaviors
apps/web/components/auth/auth-overlay.tsx
Introduce a new, self-contained LoginForm component supporting social and email-based auth flows.
  • Build a styled login/register toggle form with GitHub/Google social buttons and email/password flows
  • Wire form actions to signIn.social, signIn.email, and signUp.email with success/error handling and loading states
  • Add UX enhancements such as email domain autocomplete, password visibility toggles, and inline error display
apps/web/components/auth/login-form.tsx
Update the user menu sign-in action to open the auth modal instead of navigating to /login.
  • Consume useAuthModal from context within UserMenu
  • Replace the Sign In button link with an onClick that calls openAuthModal
apps/web/components/auth/user-menu.tsx

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 14, 2026

📝 Walkthrough

Walkthrough

This PR introduces a modal-based authentication system, shifting from a dedicated login page to a globally accessible modal. It adds authentication context, UI components for the overlay and login form with social/email auth, integrates them into the app providers, updates user navigation to trigger the modal, and redirects the /login route to open the modal on the home page.

Changes

Cohort / File(s) Summary
Login Page Replacement
apps/web/app/(auth)/login/page.tsx
Replaced full AuthPage component with LoginPage stub returning null, removing previous JSX structure and UI logic.
Auth Modal Infrastructure
apps/web/components/auth/auth-modal-context.tsx, apps/web/components/auth/auth-modal.tsx
Added AuthModalProvider context managing modal visibility state and requireAuth flag, with useAuthModal hook and openAuthModal method. AuthModal component wires context to AuthOverlay with controlled visibility and closability based on authentication requirements.
Auth Modal UI Components
apps/web/components/auth/auth-overlay.tsx, apps/web/components/auth/login-form.tsx
Introduced AuthOverlay with animated slide-up drawer, drag-to-close on mobile, body scroll locking, and focus management. Added LoginForm with GitHub/Google OAuth, email/password sign-in/register modes with password visibility toggles, email domain autocomplete, and error handling with per-provider tracking.
Integration & Routing
apps/web/providers.tsx, apps/web/components/auth/user-menu.tsx, apps/web/proxy.ts
Wrapped provider tree with AuthModalProvider and AuthModal. Updated user menu to trigger openAuthModal instead of navigating to /login. Added proxy rule to redirect /login requests to root with auth=1 query parameter, triggering modal open via URL effect.

Sequence Diagram

sequenceDiagram
    actor User
    participant UserMenu
    participant AuthModalProvider
    participant AuthOverlay
    participant LoginForm
    participant AuthService
    participant Router

    User->>UserMenu: Click "Sign In"
    UserMenu->>AuthModalProvider: useAuthModal().openAuthModal()
    AuthModalProvider->>AuthModalProvider: setShowAuthModal(true)
    AuthModalProvider->>AuthOverlay: isOpen=true
    AuthOverlay->>User: Display animated modal overlay

    User->>LoginForm: Enter email & password
    LoginForm->>LoginForm: Validate credentials
    User->>LoginForm: Click Sign In
    LoginForm->>AuthService: signIn.email({ email, password })
    AuthService->>AuthService: Authenticate user
    
    alt Success
        AuthService-->>LoginForm: onSuccess callback
        LoginForm->>AuthOverlay: onSuccess triggered
        AuthOverlay->>AuthModalProvider: setShowAuthModal(false)
        AuthModalProvider->>AuthOverlay: isOpen=false
        AuthOverlay->>User: Close modal with animation
    else Error
        AuthService-->>LoginForm: onError callback
        LoginForm->>User: Display error message
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

codex

Poem

🐰 A modal springs to life, so smooth and light,
No page redirects in the dead of night,
From any corner, auth takes the stage,
With swipes and forms on our glowing page,
One click, one drawer—authentication done bright! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
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.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(auth): in-app auth modal flow' directly and concisely summarizes the main change: introducing an in-app authentication modal flow to replace the standalone login page.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into daddy

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/auth-ui-modal-pr

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

❤️ Share

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

Copy link
Contributor

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

  • In AuthModalProvider, requireAuth is only ever set to true and never reset on close or when openAuthModal is called without requireAuth, so once an auth-required flow runs, all future openings will remain non-dismissible; consider explicitly resetting requireAuth to false on close or when calling openAuthModal without requireAuth.
  • The /app/(auth)/login/page.tsx component now returns null while /login is redirected in proxy.ts; if the redirect is guaranteed, you can simplify by removing the page entirely to avoid maintaining an unused route component.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `AuthModalProvider`, `requireAuth` is only ever set to `true` and never reset on close or when `openAuthModal` is called without `requireAuth`, so once an auth-required flow runs, all future openings will remain non-dismissible; consider explicitly resetting `requireAuth` to `false` on close or when calling `openAuthModal` without `requireAuth`.
- The `/app/(auth)/login/page.tsx` component now returns `null` while `/login` is redirected in `proxy.ts`; if the redirect is guaranteed, you can simplify by removing the page entirely to avoid maintaining an unused route component.

## Individual Comments

### Comment 1
<location> `apps/web/components/auth/auth-overlay.tsx:102` </location>
<code_context>
+export function AuthOverlay({ isOpen, onClose, onSuccess, allowClose = true }: Props) {
+	const closeButtonRef = useRef<HTMLButtonElement>(null)
+	const isMobile = useMediaQuery('(max-width: 768px)')
+	const y = useMotionValue(0)
+
+	useEffect(() => {
</code_context>

<issue_to_address>
**issue (bug_risk):** Avoid mixing an explicit motion value for `y` with `initial/animate/exit` that also drive `y`.

`y` is defined as a `useMotionValue` and passed via `style={{ y }}`, but is also controlled by `initial/animate/exit` (`initial={{ y: '100%' }}`, `animate={{ y: 0 }}`, `exit={{ y: '100%' }}`). Framer Motion expects one controller per animated property; mixing them can cause the enter/exit animation and drag behavior to conflict. Consider either:

- Dropping `useMotionValue` and relying solely on `initial/animate/exit` + `drag`, or
- Keeping `useMotionValue`/`style={{ y }}` and driving `y` only via that (e.g. `animate(y, ...)`/variants), removing `y` from `initial/animate/exit`.

This keeps `y` single-sourced and the drag/snap behavior predictable.
</issue_to_address>

### Comment 2
<location> `apps/web/components/auth/login-form.tsx:394-403` </location>
<code_context>
+	const handleSubmit = useCallback(
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Consider explicitly clearing loading state on successful email sign-in/sign-up to avoid getting stuck in a loading state in non-redirect flows.

In `handleSubmit`, `setIsLoading(true)` / `setLoadingAction('email')` are only cleared on validation errors or in `onError`/`catch`, not on success. In flows where `signIn.email` / `signUp.email` don’t navigate away (e.g. a modal where `onSuccess` just closes the overlay), the form can be left with `isLoading === true` and `loadingAction === 'email'` after a successful submit. Please also reset these flags on success (in `onSuccess` or after the `await`) so state is consistent across all flows.

Suggested implementation:

```typescript
	const handleSubmit = useCallback(
		async (e: React.FormEvent) => {
			e.preventDefault()
			setGeneralError('')
			setConfirmError('')

			setIsLoading(true)
			setLoadingAction('email')

			try {
				if (isRegisterMode) {

```

To fully implement the suggestion, you should also ensure that the `try` block for `handleSubmit` ends with a `finally` that clears the loading state, for example:

```ts
try {
  if (isRegisterMode) {
    // ... existing sign-up logic (validation, signUp.email, etc.)
  } else {
    // ... existing sign-in logic (signIn.email, etc.)
  }
} catch (err) {
  // ... existing error handling, including setGeneralError, setConfirmError, etc.
} finally {
  setIsLoading(false)
  setLoadingAction(null)
}
```

Key points:
- Keep all existing validation and error-handling logic in `try` / `catch`.
- The new `finally` will run on both success and error, ensuring `isLoading` and `loadingAction` are reset even when `signIn.email` / `signUp.email` succeeds without a redirect (e.g. modal flows where `onSuccess` only closes the overlay).
- If you already call `setIsLoading(false)` / `setLoadingAction(null)` in `catch`, you can remove those from `catch` once they're in `finally` to avoid redundant calls.
</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.

export function AuthOverlay({ isOpen, onClose, onSuccess, allowClose = true }: Props) {
const closeButtonRef = useRef<HTMLButtonElement>(null)
const isMobile = useMediaQuery('(max-width: 768px)')
const y = useMotionValue(0)
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Avoid mixing an explicit motion value for y with initial/animate/exit that also drive y.

y is defined as a useMotionValue and passed via style={{ y }}, but is also controlled by initial/animate/exit (initial={{ y: '100%' }}, animate={{ y: 0 }}, exit={{ y: '100%' }}). Framer Motion expects one controller per animated property; mixing them can cause the enter/exit animation and drag behavior to conflict. Consider either:

  • Dropping useMotionValue and relying solely on initial/animate/exit + drag, or
  • Keeping useMotionValue/style={{ y }} and driving y only via that (e.g. animate(y, ...)/variants), removing y from initial/animate/exit.

This keeps y single-sourced and the drag/snap behavior predictable.

Comment on lines +394 to +403
const handleSubmit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setLoadingAction('email')
setGeneralError('')
setConfirmError('')

try {
if (isRegisterMode) {
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Consider explicitly clearing loading state on successful email sign-in/sign-up to avoid getting stuck in a loading state in non-redirect flows.

In handleSubmit, setIsLoading(true) / setLoadingAction('email') are only cleared on validation errors or in onError/catch, not on success. In flows where signIn.email / signUp.email don’t navigate away (e.g. a modal where onSuccess just closes the overlay), the form can be left with isLoading === true and loadingAction === 'email' after a successful submit. Please also reset these flags on success (in onSuccess or after the await) so state is consistent across all flows.

Suggested implementation:

	const handleSubmit = useCallback(
		async (e: React.FormEvent) => {
			e.preventDefault()
			setGeneralError('')
			setConfirmError('')

			setIsLoading(true)
			setLoadingAction('email')

			try {
				if (isRegisterMode) {

To fully implement the suggestion, you should also ensure that the try block for handleSubmit ends with a finally that clears the loading state, for example:

try {
  if (isRegisterMode) {
    // ... existing sign-up logic (validation, signUp.email, etc.)
  } else {
    // ... existing sign-in logic (signIn.email, etc.)
  }
} catch (err) {
  // ... existing error handling, including setGeneralError, setConfirmError, etc.
} finally {
  setIsLoading(false)
  setLoadingAction(null)
}

Key points:

  • Keep all existing validation and error-handling logic in try / catch.
  • The new finally will run on both success and error, ensuring isLoading and loadingAction are reset even when signIn.email / signUp.email succeeds without a redirect (e.g. modal flows where onSuccess only closes the overlay).
  • If you already call setIsLoading(false) / setLoadingAction(null) in catch, you can remove those from catch once they're in finally to avoid redundant calls.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@apps/web/app/providers.tsx`:
- Around line 58-72: Wrap the AuthModalProvider usage in the providers tree with
a React Suspense boundary to satisfy the useSearchParams() requirement in
auth-modal-context.tsx (line 26); import Suspense from React and enclose the
<AuthModalProvider> ... </AuthModalProvider> block inside <Suspense
fallback={null}>...</Suspense> so AuthModalProvider (and its consumer AuthModal)
are rendered within a Suspense boundary while keeping the rest of the provider
tree unchanged.

In `@apps/web/components/auth/auth-overlay.tsx`:
- Around line 99-199: AuthOverlay currently implements a custom modal without a
focus trap; replace the manual overlay/motion.div + backdrop logic with the
DrawerDialog component from `@skriuw/ui` to get built-in focus trapping,
role/aria-modal, escape handling, scroll lock and mobile drag-to-close.
Specifically, render DrawerDialog in place of the outer motion.div(s) and pass
AuthOverlay props (isOpen -> open, onClose -> onClose, allowClose -> closeable)
and keep LoginForm as the dialog content (preserve the onSuccess handler to call
onSuccess and onClose); remove manual escape listener, body overflow handling,
and the custom focus management (closeButtonRef/useEffect) and reuse
DrawerDialog’s close button or still pass closeButtonRef into DrawerDialog if
needed. Ensure you forward the motion y/drag behavior only via DrawerDialog
props (or remove y if DrawerDialog handles drag) so keyboard focus cannot escape
and the root has role="dialog" and aria-modal="true".

In `@apps/web/components/auth/login-form.tsx`:
- Line 689: The terms copy currently says "By clicking 'Get Started'" which
doesn't match the actual CTA; update the string in the login-form component
(LoginForm / the JSX near the consent text) to reference the actual submit
button label used by the form (e.g., "Sign in" or "Create account") or derive it
from the same variable/prop that renders the <button> label so the copy always
matches the CTA; locate the consent paragraph in
apps/web/components/auth/login-form.tsx and replace the hardcoded "Get Started"
text with the actual button label or a reference to the buttonLabel variable.
- Around line 500-551: The inputs in the LoginForm component rely only on
placeholder text (e.g., the Name input, the email input handled by
handleEmailChange/handleEmailKeyDown, and the password/confirm-password inputs)
which is inaccessible to screen readers; add explicit, accessible labels for
each input by either inserting visually-hidden <label> elements tied to each
input via id/htmlFor or by adding descriptive aria-label attributes (and keep
ids consistent with label htmlFor), and ensure these changes are applied for
Name, Email, Password, and Confirm Password inputs inside the form handled by
handleSubmit so screen readers announce them properly while preserving current
styling and focus behavior.
🧹 Nitpick comments (6)
apps/web/app/(auth)/login/page.tsx (1)

1-3: Consider a server-side redirect as a fallback.

If middleware doesn't execute for any reason (e.g., edge cases in deployment), this page renders blank. A redirect('/?auth=1') from next/navigation here would be a safer fallback.

🔧 Proposed fix
+import { redirect } from 'next/navigation'
+
 export default function LoginPage() {
-	return null
+	redirect('/?auth=1')
 }
apps/web/components/auth/auth-modal-context.tsx (1)

33-41: Context value exposes raw setters alongside openAuthModal — potential for inconsistent state.

Consumers can call setShowAuthModal(true) directly, bypassing the requireAuth reset that openAuthModal performs. This could leave requireAuth stale from a previous invocation. Consider narrowing the public API to only openAuthModal and a closeAuthModal helper, keeping the raw setters internal.

♻️ Sketch
 type AuthModalContextValue = {
 	showAuthModal: boolean
-	setShowAuthModal: (show: boolean) => void
 	requireAuth: boolean
-	setRequireAuth: (requireAuth: boolean) => void
 	openAuthModal: (options?: OpenAuthModalOptions) => void
+	closeAuthModal: () => void
 }

Then in the provider:

+const closeAuthModal = useCallback(() => {
+	setShowAuthModal(false)
+	setRequireAuth(false)
+}, [])
apps/web/components/auth/auth-overlay.tsx (1)

16-97: Inline style objects deviate from the project's Tailwind-first convention.

The coding guidelines specify "Prefer utility-first styling and existing semantic classes with Tailwind v4" and "Follow existing tokens and CSS variables." This component defines ~80 lines of inline style objects instead of using Tailwind classes. While the CSS variables usage is correct, the delivery mechanism doesn't match the project convention.

This is a sizable refactor so it doesn't need to block the PR, but consider migrating to Tailwind classes in a follow-up to stay consistent with the rest of the codebase.

apps/web/components/auth/login-form.tsx (3)

43-193: ~150 lines of inline style objects — same Tailwind convention concern as in auth-overlay.tsx.

Both this file and auth-overlay.tsx define extensive inline style objects rather than using Tailwind utility classes. Consider migrating in a follow-up pass to stay consistent with the project's styling approach. As per coding guidelines, "Prefer utility-first styling and existing semantic classes with Tailwind v4."


226-294: AuthButton is a one-off primitive — consider reusing Button from @skriuw/ui.

The project already has a Button component (imported in user-menu.tsx from @skriuw/ui/button). AuthButton re-implements button styling, disabled state, and loading animation from scratch. Consider extending @skriuw/ui/button with an isLoading prop or composing it instead of rolling a new primitive. As per coding guidelines, "Reuse @skriuw/ui components before creating one-off primitives."


350-392: Social sign-in handlers are near-identical — consider deduplicating.

handleGithubSignIn and handleGoogleSignIn only differ in the provider string and error message. Extract a shared handleSocialSignIn(provider: string) to reduce duplication.

♻️ Sketch
+const handleSocialSignIn = useCallback(async (provider: 'github' | 'google') => {
+	setIsLoading(true)
+	setLoadingAction(provider)
+	setGeneralError('')
+	try {
+		await signIn.social(
+			{ provider },
+			{
+				onError: (ctx) => {
+					setGeneralError(ctx.error.message || `Failed to sign in with ${provider}.`)
+					setIsLoading(false)
+					setLoadingAction(null)
+				}
+			}
+		)
+	} catch (err) {
+		setGeneralError(err instanceof Error ? err.message : `Failed to sign in with ${provider}.`)
+		setIsLoading(false)
+		setLoadingAction(null)
+	}
+}, [])

Comment on lines +58 to +72
<AuthModalProvider>
<SettingsProvider>
<NotesProvider>
<ShortcutProvider>
<ContextMenuProvider>
<EditorTabsProvider>
<AppLayoutManager>{children}</AppLayoutManager>
</EditorTabsProvider>
</ContextMenuProvider>
<CommandExecutor />
</ShortcutProvider>
</NotesProvider>
</SettingsProvider>
<AuthModal />
</AuthModalProvider>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# 1. Check if auth-modal-context.tsx exists and uses useSearchParams()
fd 'auth-modal' apps/web --type f | head -20

Repository: remcostoeten/skriuw

Length of output: 150


🏁 Script executed:

# 2. Read the full providers.tsx file to understand context
wc -l apps/web/app/providers.tsx

Repository: remcostoeten/skriuw

Length of output: 92


🏁 Script executed:

# 3. Check for Suspense usage in the app directory
rg -n 'Suspense' --type=tsx --type=ts apps/web/app/ | head -30

Repository: remcostoeten/skriuw

Length of output: 90


🏁 Script executed:

# 4. Check how Providers is used in layout files
rg -n -B2 -A2 '<Providers' apps/web/app/layout.tsx

Repository: remcostoeten/skriuw

Length of output: 234


🏁 Script executed:

# Read the auth-modal-context.tsx file to check for useSearchParams()
cat -n apps/web/components/auth/auth-modal-context.tsx

Repository: remcostoeten/skriuw

Length of output: 2354


🏁 Script executed:

# Read the full providers.tsx file
cat -n apps/web/app/providers.tsx

Repository: remcostoeten/skriuw

Length of output: 3178


🏁 Script executed:

# Check the root layout file for Suspense wrapping
cat -n apps/web/app/layout.tsx | head -100

Repository: remcostoeten/skriuw

Length of output: 2821


🏁 Script executed:

# Try Suspense search with correct file type flag
rg 'Suspense' apps/web/app/ -A2 -B2 | head -50

Repository: remcostoeten/skriuw

Length of output: 45


AuthModalProvider uses useSearchParams() — add a <Suspense> boundary for proper Next.js streaming support.

AuthModalProvider calls useSearchParams() at line 26 of auth-modal-context.tsx, which requires a <Suspense> boundary in Next.js App Router. While the route-level export const dynamic = 'force-dynamic' in layout.tsx mitigates the runtime issue by forcing dynamic rendering, wrapping AuthModalProvider in Suspense is still recommended for:

  • Proper streaming and content delivery
  • Following Next.js best practices
  • Future-proofing in case force-dynamic is removed
🔧 Proposed fix
+import { Suspense } from 'react'
 // ... existing imports
 
 <StorageInitializer>
+  <Suspense>
     <AuthModalProvider>
       <SettingsProvider>
         ...
       </SettingsProvider>
       <AuthModal />
     </AuthModalProvider>
+  </Suspense>
 </StorageInitializer>
🤖 Prompt for AI Agents
In `@apps/web/app/providers.tsx` around lines 58 - 72, Wrap the AuthModalProvider
usage in the providers tree with a React Suspense boundary to satisfy the
useSearchParams() requirement in auth-modal-context.tsx (line 26); import
Suspense from React and enclose the <AuthModalProvider> ... </AuthModalProvider>
block inside <Suspense fallback={null}>...</Suspense> so AuthModalProvider (and
its consumer AuthModal) are rendered within a Suspense boundary while keeping
the rest of the provider tree unchanged.

Comment on lines +99 to +199
export function AuthOverlay({ isOpen, onClose, onSuccess, allowClose = true }: Props) {
const closeButtonRef = useRef<HTMLButtonElement>(null)
const isMobile = useMediaQuery('(max-width: 768px)')
const y = useMotionValue(0)

useEffect(() => {
if (isOpen && closeButtonRef.current && allowClose) {
closeButtonRef.current.focus()
}
}, [isOpen, allowClose])

useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen && allowClose && onClose) {
onClose()
}
}
document.addEventListener('keydown', handleEscape)
return () => document.removeEventListener('keydown', handleEscape)
}, [isOpen, allowClose, onClose])

useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => {
document.body.style.overflow = ''
}
}, [isOpen])

useEffect(() => {
if (!isOpen) y.set(0)
}, [isOpen, y])

return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
style={styles.overlay}
>
<div style={styles.backdrop} onClick={allowClose && onClose ? onClose : undefined} />

<motion.div
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
drag={isMobile && allowClose ? 'y' : false}
dragConstraints={{ top: 0, bottom: 0 }}
dragElastic={0.2}
onDragEnd={(_, info) => {
if (!isMobile || !allowClose) return
const shouldClose =
info.offset.y > window.innerHeight * 0.15 || info.velocity.y > 500
if (shouldClose) {
onClose?.()
return
}
animate(y, 0, { duration: 0.2, ease: 'easeOut' })
}}
style={{
...styles.drawer,
y
}}
>
{isMobile && allowClose && (
<div style={styles.dragHandle}>
<div style={styles.dragHandleBar} aria-label='Drag to close' />
</div>
)}

<div style={styles.contentWrapper}>
{allowClose && onClose && (
<button
ref={closeButtonRef}
onClick={onClose}
style={styles.closeButton}
aria-label='Close'
>
<X style={{ width: 20, height: 20 }} />
</button>
)}

<LoginForm
onSuccess={() => {
onSuccess?.()
onClose?.()
}}
/>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the project has a Dialog component in `@skriuw/ui` or a focus-trap dependency
echo "=== Looking for Dialog in `@skriuw/ui` ==="
fd -t f -e tsx -e ts 'dialog' packages/ui/ 2>/dev/null | head -20

echo ""
echo "=== Looking for focus-trap in package.json ==="
cat package.json | grep -i 'focus-trap' || echo "No focus-trap found in root package.json"

echo ""
echo "=== Checking packages/ui/package.json ==="
if [ -f packages/ui/package.json ]; then
  cat packages/ui/package.json | grep -i 'focus-trap' || echo "No focus-trap in packages/ui/package.json"
fi

echo ""
echo "=== Looking for aria-modal usage patterns ==="
rg -n 'aria-modal' --type tsx --type ts | head -20

echo ""
echo "=== Checking for existing modal implementations ==="
rg -l 'role.*dialog' --type tsx --type ts | head -10

echo ""
echo "=== Checking auth-overlay.tsx styling approach ==="
head -100 apps/web/components/auth/auth-overlay.tsx | grep -A5 'style='

Repository: remcostoeten/skriuw

Length of output: 615


🏁 Script executed:

#!/bin/bash
# Examine the existing dialog components
echo "=== dialog.tsx (first 80 lines) ==="
head -80 packages/ui/dialog.tsx

echo ""
echo "=== dialog-drawer.tsx (first 80 lines) ==="
head -80 packages/ui/dialog-drawer.tsx

echo ""
echo "=== auth-overlay.tsx complete styling section ==="
sed -n '16,98p' apps/web/components/auth/auth-overlay.tsx

Repository: remcostoeten/skriuw

Length of output: 7042


🏁 Script executed:

#!/bin/bash
# Check full dialog-drawer implementation and its features
echo "=== dialog-drawer.tsx (complete file) ==="
wc -l packages/ui/dialog-drawer.tsx

echo ""
echo "=== Check for focus trap creation in dialog-drawer ==="
rg -A5 'createFocusTrap' packages/ui/dialog-drawer.tsx

echo ""
echo "=== Check exports from dialog-drawer ==="
rg 'export' packages/ui/dialog-drawer.tsx

echo ""
echo "=== Check mobile/drag features in dialog-drawer ==="
rg -n 'drag|mobile|Mobile' packages/ui/dialog-drawer.tsx | head -20

Repository: remcostoeten/skriuw

Length of output: 2595


Missing focus trap — keyboard focus can escape the open modal.

When the overlay is open, pressing Tab moves focus to background elements behind the modal. Modal dialogs require a focus trap to keep keyboard navigation within the overlay. Additionally, the root element should have role="dialog" and aria-modal="true".

Rather than adding a focus-trap library, use the existing DrawerDialog component from @skriuw/ui, which already provides focus trap via createFocusTrap, handles Escape key, scroll lock, role/aria-modal, and supports mobile drag-to-close. This aligns with the guideline: "Reuse @skriuw/ui components before creating one-off primitives."

🤖 Prompt for AI Agents
In `@apps/web/components/auth/auth-overlay.tsx` around lines 99 - 199, AuthOverlay
currently implements a custom modal without a focus trap; replace the manual
overlay/motion.div + backdrop logic with the DrawerDialog component from
`@skriuw/ui` to get built-in focus trapping, role/aria-modal, escape handling,
scroll lock and mobile drag-to-close. Specifically, render DrawerDialog in place
of the outer motion.div(s) and pass AuthOverlay props (isOpen -> open, onClose
-> onClose, allowClose -> closeable) and keep LoginForm as the dialog content
(preserve the onSuccess handler to call onSuccess and onClose); remove manual
escape listener, body overflow handling, and the custom focus management
(closeButtonRef/useEffect) and reuse DrawerDialog’s close button or still pass
closeButtonRef into DrawerDialog if needed. Ensure you forward the motion y/drag
behavior only via DrawerDialog props (or remove y if DrawerDialog handles drag)
so keyboard focus cannot escape and the root has role="dialog" and
aria-modal="true".

Comment on lines +500 to +551
<form onSubmit={handleSubmit} style={styles.form}>
{isRegisterMode && (
<div style={styles.inputWrapper}>
<input
type='text'
placeholder='Name'
value={name}
onChange={(e) => setName(e.target.value)}
style={styles.input}
autoComplete='name'
/>
</div>
)}

<div style={styles.inputWrapper}>
{emailSuggestion && emailSuggestion.startsWith(email) && (
<>
<div style={ghostInputStyle} aria-hidden='true'>
<span style={{ opacity: 0 }}>{email}</span>
<span style={{ opacity: 0.5 }}>{emailSuggestion.slice(email.length)}</span>
</div>
<div
style={{
position: 'absolute',
right: '12px',
top: '50%',
transform: 'translateY(-50%)',
zIndex: 10,
opacity: 0.7
}}
>
<Kbd>Tab</Kbd>
</div>
</>
)}
<input
type='email'
placeholder='Email address'
value={email}
onChange={handleEmailChange}
onKeyDown={handleEmailKeyDown}
required
autoComplete='email'
style={{
...styles.input,
position: 'relative',
zIndex: 1,
background: 'transparent',
...(generalError ? styles.inputError : {})
}}
/>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Form inputs rely on placeholder text without visible <label> elements.

Screen readers and assistive technologies depend on explicit labels. Using only placeholder is insufficient — it disappears on input and isn't consistently announced. Add visually-hidden labels or use aria-label on each input at minimum.

As per coding guidelines, "Maintain accessible interactions: focus visibility, keyboard navigation, labels."

🔧 Example for the email input
 <div style={styles.inputWrapper}>
+  <label htmlFor="login-email" className="sr-only">Email address</label>
   <input
+    id="login-email"
     type='email'
     placeholder='Email address'

Apply the same pattern for Name, Password, and Confirm Password inputs.

📝 Committable suggestion

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

Suggested change
<form onSubmit={handleSubmit} style={styles.form}>
{isRegisterMode && (
<div style={styles.inputWrapper}>
<input
type='text'
placeholder='Name'
value={name}
onChange={(e) => setName(e.target.value)}
style={styles.input}
autoComplete='name'
/>
</div>
)}
<div style={styles.inputWrapper}>
{emailSuggestion && emailSuggestion.startsWith(email) && (
<>
<div style={ghostInputStyle} aria-hidden='true'>
<span style={{ opacity: 0 }}>{email}</span>
<span style={{ opacity: 0.5 }}>{emailSuggestion.slice(email.length)}</span>
</div>
<div
style={{
position: 'absolute',
right: '12px',
top: '50%',
transform: 'translateY(-50%)',
zIndex: 10,
opacity: 0.7
}}
>
<Kbd>Tab</Kbd>
</div>
</>
)}
<input
type='email'
placeholder='Email address'
value={email}
onChange={handleEmailChange}
onKeyDown={handleEmailKeyDown}
required
autoComplete='email'
style={{
...styles.input,
position: 'relative',
zIndex: 1,
background: 'transparent',
...(generalError ? styles.inputError : {})
}}
/>
</div>
<form onSubmit={handleSubmit} style={styles.form}>
{isRegisterMode && (
<div style={styles.inputWrapper}>
<label htmlFor="login-name" className="sr-only">Name</label>
<input
id="login-name"
type='text'
placeholder='Name'
value={name}
onChange={(e) => setName(e.target.value)}
style={styles.input}
autoComplete='name'
/>
</div>
)}
<div style={styles.inputWrapper}>
<label htmlFor="login-email" className="sr-only">Email address</label>
{emailSuggestion && emailSuggestion.startsWith(email) && (
<>
<div style={ghostInputStyle} aria-hidden='true'>
<span style={{ opacity: 0 }}>{email}</span>
<span style={{ opacity: 0.5 }}>{emailSuggestion.slice(email.length)}</span>
</div>
<div
style={{
position: 'absolute',
right: '12px',
top: '50%',
transform: 'translateY(-50%)',
zIndex: 10,
opacity: 0.7
}}
>
<Kbd>Tab</Kbd>
</div>
</>
)}
<input
id="login-email"
type='email'
placeholder='Email address'
value={email}
onChange={handleEmailChange}
onKeyDown={handleEmailKeyDown}
required
autoComplete='email'
style={{
...styles.input,
position: 'relative',
zIndex: 1,
background: 'transparent',
...(generalError ? styles.inputError : {})
}}
/>
</div>
🤖 Prompt for AI Agents
In `@apps/web/components/auth/login-form.tsx` around lines 500 - 551, The inputs
in the LoginForm component rely only on placeholder text (e.g., the Name input,
the email input handled by handleEmailChange/handleEmailKeyDown, and the
password/confirm-password inputs) which is inaccessible to screen readers; add
explicit, accessible labels for each input by either inserting visually-hidden
<label> elements tied to each input via id/htmlFor or by adding descriptive
aria-label attributes (and keep ids consistent with label htmlFor), and ensure
these changes are applied for Name, Email, Password, and Confirm Password inputs
inside the form handled by handleSubmit so screen readers announce them properly
while preserving current styling and focus behavior.

lineHeight: '1.5'
}}
>
By clicking &quot;Get Started&quot; (or signing in), you acknowledge that you have read and
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

User-facing text mismatch: "Get Started" doesn't match any button label.

The terms copy says "By clicking 'Get Started'" but the actual submit button reads "Sign in" or "Create account." This will confuse users. Update the copy to match the actual CTA.

✏️ Proposed fix
-			By clicking &quot;Get Started&quot; (or signing in), you acknowledge that you have read and
+			By continuing, you acknowledge that you have read and
🤖 Prompt for AI Agents
In `@apps/web/components/auth/login-form.tsx` at line 689, The terms copy
currently says "By clicking 'Get Started'" which doesn't match the actual CTA;
update the string in the login-form component (LoginForm / the JSX near the
consent text) to reference the actual submit button label used by the form
(e.g., "Sign in" or "Create account") or derive it from the same variable/prop
that renders the <button> label so the copy always matches the CTA; locate the
consent paragraph in apps/web/components/auth/login-form.tsx and replace the
hardcoded "Get Started" text with the actual button label or a reference to the
buttonLabel variable.

@remcostoeten remcostoeten merged commit c8e1e8d into daddy Feb 14, 2026
5 of 6 checks passed
@remcostoeten remcostoeten deleted the feature/auth-ui-modal-pr branch February 14, 2026 16:24
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