Skip to content

feat: add loading states and refactor to TSX#40

Merged
dnywh merged 5 commits into
mainfrom
dnywh/feat/button-loading-states
Apr 12, 2026
Merged

feat: add loading states and refactor to TSX#40
dnywh merged 5 commits into
mainfrom
dnywh/feat/button-loading-states

Conversation

@dnywh
Copy link
Copy Markdown
Owner

@dnywh dnywh commented Apr 12, 2026

Standardises button loading and async feedback. Refactors any JSX file touched to TSX.

Summary

Fix button feedback by standardising async state ownership first, then reflecting that state in the right button primitive. Text buttons use text-only loading labels. Icon buttons keep their fixed footprint and replace the icon with a spinner while updating accessible labels.

Public API changes

  • Convert touched button files to TSX: Button, SubmitButton, IconButton, and ButtonToDialog, plus consumers changed for async behaviour.
  • Button remains the text-button primitive with loading and loadingText.
    • loading disables real buttons, sets aria-disabled, and sets aria-busy.
    • Text content changes to loadingText; no spinner for text buttons.
    • Link buttons do not get automatic loading behaviour.
  • SubmitButton uses useFormStatus() for server-action forms and accepts explicit loading.
    • Keep pendingText as a compatibility alias for loadingText.
    • Pass resolved loading state through to Button.
  • IconButton gets loading and loadingLabel.
    • Spinner replaces the icon inside the existing circular dimensions.
    • No visible loading text, no width change, no layout shift.
    • Require/derive an accessible label for icon-only buttons.
  • ButtonToDialog supports confirmLoadingText.
    • Server-action confirmations rely on SubmitButton.
    • Async onSubmit confirmations get local pending state and prevent double-submit.

Implementation changes

  • Fix async lifecycle bugs before wiring UI:
    • Reset submit state on validation failures and caught errors.
    • Do not rely on stale React state inside finally.
    • Keep loading active only when redirect/navigation is expected to take over.
  • Update high-impact async flows:
    • Auth and account forms: sign in/sign up keep existing copy; forgot/reset password and profile edit forms gain real pending feedback.
    • Profile actions: sign out shows Signing out...; delete account dialog shows Deleting....
    • Listing write: add/edit listing preserves Adding... / Saving...; delete listing dialog shows Deleting...; validation failures restore the button.
    • Chat: sending state lives in ChatWindow; composer textarea/send icon disable during send; send icon shows spinner with loadingLabel="Sending...".
    • Media: photo upload routes through Button loading; photo deletion tracks the specific filename and shows Deleting... only on that button; avatar upload/delete disables related controls while work is active.
    • Clipboard copy: show short Copying..., then Copied; handle failures without leaving the button stuck.
  • Leave synchronous or purely navigational controls alone:
    • Add item/link buttons, cancel/edit buttons, drawer/map open-close controls, static CTA links, and external map links.
    • Do not introduce route-transition loading for ordinary href buttons in this pass.

Testing

  • Run npm run build for TypeScript, JSX-to-TSX, and Next.js validation.
  • Run npm run format:check; if implementation mode permits formatting, run npm run format.
  • Manually verify:
    • Auth submit buttons show loading and recover on errors.
    • Profile edit buttons prevent duplicate submits and show existing pending copy.
    • Sign out and destructive dialog confirmations show clear pending text.
    • Listing validation errors restore submit buttons; successful saves navigate normally.
    • Chat send cannot double-submit and the icon spinner stays fixed-size.
    • Photo/avatar upload and delete controls cannot be double-tapped.
    • Static links and simple UI controls are unchanged.

Notes

  • JSX-to-TSX conversion is limited to touched files.
  • Text buttons use text-only loading labels.
  • Icon buttons use spinner-only visual feedback with accessible loading labels.
  • Async correctness is the priority; visual loading states should expose real pending state, not simulate it.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 12, 2026

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

Project Deployment Actions Updated (UTC)
peels Ready Ready Preview, Comment Apr 12, 2026 4:22am

@supabase
Copy link
Copy Markdown

supabase Bot commented Apr 12, 2026

This pull request has been ignored for the connected project mfnaqdyunuafbwukbbyr because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Standardises async/loading feedback across the app’s button primitives (text buttons, submit buttons, and icon-only buttons), while converting touched components from JSX to TSX to improve type-safety and consistency across higher-impact async flows (auth/profile/listings/chat/media/clipboard).

Changes:

  • Refactors Button/SubmitButton/IconButton to support consistent loading/disabled semantics and accessible labeling, and converts them to TSX with typed props.
  • Updates key async UX flows (profile actions, listings write/delete, chat send, media upload/delete, clipboard copy) to own pending state correctly and reflect it in the appropriate button primitive.
  • Enhances confirmation dialogs (ButtonToDialog) to support configurable loading labels and prevent double-submit for client onSubmit confirmations.

Reviewed changes

Copilot reviewed 16 out of 28 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/components/Button/Button.tsx Adds typed props + standardised loading/disabled behavior for the base button primitive (button vs link).
src/components/SubmitButton/SubmitButton.tsx Implements loading state via useFormStatus() and compatibility aliasing (pendingTextloadingText).
src/components/IconButton/IconButton.tsx Adds icon-button loading spinner behavior and accessible label handling.
src/components/ButtonToDialog/ButtonToDialog.tsx Adds confirm loading text support + local pending state for async onSubmit confirmations.
src/components/ListingPhotosManager/ListingPhotosManager.tsx Adds per-photo delete loading and upload loading feedback; introduces drag/drop typing.
src/components/ListingWrite/ListingWrite.tsx Improves async lifecycle correctness (validation/reset, navigation-aware loading) and TS typing.
src/components/ChatWindow/ChatWindow.tsx Moves sending state ownership into ChatWindow and wires through to the composer.
src/components/ChatComposer/ChatComposer.tsx Disables composer controls during send and uses IconButton loading label/spinner.
src/components/ProfileActions/ProfileActions.tsx Adds sign-out loading feedback and confirm-loading text for destructive dialogs.
src/components/ProfileAccountSettings/ProfileAccountSettings.tsx Adds explicit loading wiring for submit actions and improves TS typing.
src/components/EmailSelector/EmailSelector.tsx Adds “copying/copied/error” async copy feedback via button loading.
src/components/AvatarUploadView/AvatarUploadView.tsx Adds upload/delete busy handling + button loading text.
src/app/(forms)/forgot-password/page.tsx Updates submit button pending copy (“Emailing...”).
src/app/(forms)/profile/reset-password/page.tsx Updates submit button pending copy (“Resetting...”).
src/**/index.ts Adds barrel exports for updated components.
Comments suppressed due to low confidence (5)

src/components/Button/Button.tsx:234

  • For the Link-rendering branch, aria-disabled/tabIndex don’t prevent mouse clicks, so a “disabled” Button with href will still navigate. Also disabled isn’t a valid attribute for anchors/Next Link. Consider omitting href when isDisabled is true, or add an onClick that calls preventDefault() (and maybe stopPropagation()) when disabled, while keeping styling via a data-attribute.
    src/components/ListingPhotosManager/ListingPhotosManager.tsx:173
  • handleDrop builds newPhotos from the captured photos array after async compression/uploads. If photos are reordered/deleted while the upload is in flight, this can drop those updates. Prefer functional state updates (e.g., setPhotos(prev => [...prev, ...newFilenames])) and derive the value passed to onPhotosChange from that same update, or temporarily disable reordering/deletes while uploading.
    src/components/ListingPhotosManager/ListingPhotosManager.tsx:216
  • In the delete error path, setPhotos(photos) reverts to the stale pre-delete array captured in the closure, which can overwrite legitimate changes that happened while the delete request was in flight (e.g., uploads/reordering). Consider restoring just the deleted filename (if missing) or tracking a snapshot in a ref, and/or disabling other photo mutations while deletingPhoto is set.
    src/components/EmailSelector/EmailSelector.tsx:124
  • This introduces a non-translated UI string ("Copy failed") in a component that otherwise uses next-intl messages. Consider adding a Contact.copyButton.copyFailed (or similar) key in the locale message files and using t(...) here so non-English locales don’t show an English fallback.
    src/components/ProfileAccountSettings/ProfileAccountSettings.tsx:208
  • Typo in function name: handleNewslettePreferenceUpdate (missing “r”). Renaming to handleNewsletterPreferenceUpdate will improve clarity and avoid propagating the typo to future references.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

: ariaLabel || iconLabels[icon];

const handleClick: React.MouseEventHandler<HTMLElement> = (e) => {
if (loading || disabled) return;
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR standardizes async/loading feedback across the app’s button primitives (text vs icon buttons) and refactors touched UI components from JSX to TSX, while also tightening async lifecycle handling (preventing double-submits and “stuck” pending states).

Changes:

  • Introduces/updates button primitives (Button, SubmitButton, IconButton, ButtonToDialog) to provide consistent loading/disabled/ARIA behavior.
  • Refactors key UI flows (profile actions/settings, listing write + media managers, chat send, email copy) to own async state correctly and reflect it in buttons.
  • Adds i18n strings and performs small formatting/TSX conversions in Supabase function templates and utility scripts.

Reviewed changes

Copilot reviewed 28 out of 40 changed files in this pull request and generated no comments.

Show a summary per file
File Description
supabase/functions/send-email-for-auth-action/index.ts Formatting-only refactor for readability in webhook email function.
supabase/functions/_templates/sign-up-email.tsx Template formatting; no behavioral change.
supabase/functions/_templates/reset-password-email.tsx Template formatting; no behavioral change.
supabase/functions/_templates/magic-link-email.tsx Template formatting; no behavioral change.
supabase/functions/_templates/invite-email.tsx Template formatting; no behavioral change.
supabase/functions/_templates/email-change-email.tsx Template formatting; no behavioral change.
src/proxy.ts Formatting-only changes around cookie initialization.
src/components/SubmitButton/SubmitButton.tsx New TSX SubmitButton integrating useFormStatus() and explicit loading state.
src/components/SubmitButton/SubmitButton.jsx Removes old JSX implementation.
src/components/SubmitButton/index.ts Adds barrel exports for SubmitButton.
src/components/ProfileActions/ProfileActions.tsx Adds sign-out loading state + confirm loading text for destructive action.
src/components/ProfileActions/index.ts Adds barrel exports for ProfileActions.
src/components/ProfileAccountSettings/ProfileAccountSettings.tsx TS typing improvements + wiring SubmitButton loading to update states.
src/components/ProfileAccountSettings/index.ts Adds barrel exports for ProfileAccountSettings.
src/components/ListingWrite/ListingWrite.tsx Refactors to TSX, fixes async submission lifecycle, adds confirm loading text for delete.
src/components/ListingWrite/index.ts Adds barrel exports for ListingWrite.
src/components/ListingPhotosManager/ListingPhotosManager.tsx Adds per-photo delete loading, prevents concurrent mutations, and refactors async flows.
src/components/ListingPhotosManager/index.ts Adds barrel exports for ListingPhotosManager.
src/components/IconButton/index.ts Adds barrel exports for IconButton.
src/components/IconButton/IconButton.tsx New TSX IconButton with spinner replacement + accessible loading label support.
src/components/IconButton/IconButton.jsx Removes old JSX implementation.
src/components/EmailSelector/index.ts Adds barrel exports for EmailSelector.
src/components/EmailSelector/EmailSelector.tsx Adds async copy feedback (copying/copied/error) and button loading text.
src/components/ChatWindow/index.ts Adds barrel exports for ChatWindow.
src/components/ChatWindow/ChatWindow.tsx Adds isSending state, prevents double-send, and threads loading through to composer.
src/components/ChatComposer/index.ts Adds barrel exports for ChatComposer.
src/components/ChatComposer/ChatComposer.tsx Replaces text submit with IconButton loading spinner and proper aria labels/disable rules.
src/components/ButtonToDialog/index.ts Adds barrel exports for ButtonToDialog.
src/components/ButtonToDialog/ButtonToDialog.tsx Adds confirm loading text support + local pending state for async onSubmit.
src/components/Button/index.ts Adds barrel exports for Button.
src/components/Button/Button.tsx Refactors to TSX and standardizes loading behavior (disabled + aria) for real buttons (not links).
src/components/AvatarUploadView/index.ts Adds barrel exports for AvatarUploadView.
src/components/AvatarUploadView/AvatarUploadView.tsx Adds upload/delete pending states and disables controls while busy.
src/app/(forms)/profile/reset-password/page.tsx Adds pending copy (“Resetting…”) via SubmitButton.
src/app/(forms)/forgot-password/page.tsx Adds pending copy (“Emailing…”) via SubmitButton.
src/app/(forms)/auth/complete/page.tsx Minor text formatting only.
scripts/seed-local-media.mjs Formatting-only refactor for readability.
messages/es.json Adds Contact page translations (and copy-state strings).
messages/en.json Adds copy-state strings for EmailSelector (“Copying…”, “Copy failed”).
messages/de.json Adds Contact page translations (and copy-state strings).
Comments suppressed due to low confidence (3)

src/components/ProfileActions/ProfileActions.tsx:56

  • handleSignOut wraps signOutAction() (which redirects) in a try/catch and swallows any thrown error. In Next.js, redirect() is implemented via a thrown redirect error; catching it can prevent the intended navigation and leave the UI stuck in a loading state. Consider removing the catch, or rethrowing redirect errors while only handling genuine failures (and/or switch to a <form action={signOutAction}> + SubmitButton pattern so the redirect/pending state is handled by the framework).
    src/components/ListingPhotosManager/ListingPhotosManager.tsx:198
  • When an upload fails with a 413, the alert always uses the plural message even if the user only attempted a single file. Since files is in scope here, you can pick the singular vs plural message the same way you do for the preflight size check.
    src/components/ListingPhotosManager/ListingPhotosManager.tsx:228
  • On delete failure, the UI restore appends the deleted photo to the end of the list ([...currentPhotos, photoToDelete]), which can change ordering compared to the pre-delete state. If ordering matters (and especially with drag-and-drop enabled), capture the original index (or previous array snapshot) before removing and restore it at the same position on failure.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Standardizes async/loading ownership across button primitives (text + icon) and propagates real pending state through higher-impact user flows, while converting touched components from JSX to TSX for stronger typing and consistency.

Changes:

  • Introduces standardized loading APIs for Button, SubmitButton, and IconButton, and threads them through dialogs/forms/chat/media flows.
  • Refactors several UI components to TSX and adds barrel index.ts exports for component folders.
  • Improves async lifecycle handling in a few flows (e.g., prevent double submits, reset pending state on failures).

Reviewed changes

Copilot reviewed 28 out of 40 changed files in this pull request and generated no comments.

Show a summary per file
File Description
supabase/functions/send-email-for-auth-action/index.ts Formatting / readability adjustments in the auth email hook function.
supabase/functions/_templates/sign-up-email.tsx Minor JSX formatting cleanup in email template.
supabase/functions/_templates/reset-password-email.tsx Minor JSX formatting cleanup in email template.
supabase/functions/_templates/magic-link-email.tsx Minor JSX formatting cleanup in email template.
supabase/functions/_templates/invite-email.tsx Minor JSX formatting cleanup in email template.
supabase/functions/_templates/email-change-email.tsx Minor JSX formatting cleanup in email template.
src/proxy.ts Formatting-only change in cookie-setting condition/block.
src/components/SubmitButton/SubmitButton.tsx New TSX SubmitButton using useFormStatus() + explicit loading support.
src/components/SubmitButton/SubmitButton.jsx Removes old JSX implementation of SubmitButton.
src/components/SubmitButton/index.ts Adds barrel exports for SubmitButton.
src/components/ProfileActions/ProfileActions.tsx Switches sign-out to server-action form submit + loading text; adds confirm loading text for delete.
src/components/ProfileActions/index.ts Adds barrel exports for ProfileActions.
src/components/ProfileAccountSettings/ProfileAccountSettings.tsx TS typing improvements + wires SubmitButton loading state for profile updates.
src/components/ProfileAccountSettings/index.ts Adds barrel exports for ProfileAccountSettings.
src/components/ListingWrite/ListingWrite.tsx Improves submit/delete async handling, typing, and wires dialog confirm loading copy.
src/components/ListingWrite/index.ts Adds barrel exports for ListingWrite.
src/components/ListingPhotosManager/ListingPhotosManager.tsx Adds fixed-footprint loading/disable states for photo upload/delete + safer state updates during mutations.
src/components/ListingPhotosManager/index.ts Adds barrel exports for ListingPhotosManager.
src/components/IconButton/index.ts Adds barrel exports for IconButton.
src/components/IconButton/IconButton.tsx New TSX icon-only button with spinner-on-loading + accessible loading labels.
src/components/IconButton/IconButton.jsx Removes old JSX implementation of IconButton.
src/components/EmailSelector/index.ts Adds barrel exports for EmailSelector.
src/components/EmailSelector/EmailSelector.tsx Adds async copy state (copying/copied/error) and uses Button loading UI.
src/components/ChatWindow/index.ts Adds barrel exports for ChatWindow.
src/components/ChatWindow/ChatWindow.tsx Moves “sending” ownership into ChatWindow, adds pending protections, and threads state to composer.
src/components/ChatComposer/index.ts Adds barrel exports for ChatComposer.
src/components/ChatComposer/ChatComposer.tsx Switches send control to IconButton loading spinner + disables textarea during send.
src/components/ButtonToDialog/index.ts Adds barrel exports for ButtonToDialog.
src/components/ButtonToDialog/ButtonToDialog.tsx Adds confirmLoadingText + local pending state for async onSubmit dialogs.
src/components/Button/index.ts Adds barrel exports for Button.
src/components/Button/Button.tsx Refactors to TSX, adds text-only loading behavior + aria attributes, and forwards refs.
src/components/AvatarUploadView/index.ts Adds barrel exports for AvatarUploadView.
src/components/AvatarUploadView/AvatarUploadView.tsx Adds busy/disable behavior and loading labels for avatar upload/delete actions.
src/app/(forms)/profile/reset-password/page.tsx Adds pendingText to reset-password submit flow.
src/app/(forms)/forgot-password/page.tsx Adds pendingText to forgot-password submit flow.
src/app/(forms)/auth/complete/page.tsx Minor text formatting tweak.
scripts/seed-local-media.mjs Formatting-only changes for readability.
messages/es.json Adds Spanish Contact translations (including copy loading/failure strings).
messages/en.json Adds copying/copyFailed strings for Contact copy button.
messages/de.json Adds German Contact translations (including copy loading/failure strings).
Comments suppressed due to low confidence (2)

src/components/Button/Button.tsx:37

  • ButtonProps is currently defined as an intersection of both ButtonHTMLAttributes and AnchorHTMLAttributes. When href is provided, ...props is forwarded to StyledLink, which means button-only attributes (e.g. formAction, formMethod, name, etc.) are still type-allowed and can end up rendered on an <a> as invalid/meaningless attributes. Consider changing ButtonProps to a discriminated union based on href (or separate ButtonProps/LinkButtonProps) so TS prevents passing button-only props to link buttons (and vice versa), and so the spread onto <Link> can’t accidentally include form-only attributes.
    src/components/ListingWrite/ListingWrite.tsx:231
  • For residential listings, the first-name update check compares profile.first_name !== name, but name can include transient whitespace while the value you actually send to updateFirstNameAction is validatedName (trimmed). This can trigger unnecessary update calls (and related UI states) when the only difference is whitespace. Consider basing the comparison on validatedName (or comparing trimmed values) so the update runs only when the persisted value would actually change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@dnywh dnywh merged commit 9219630 into main Apr 12, 2026
4 checks passed
@dnywh dnywh deleted the dnywh/feat/button-loading-states branch April 12, 2026 04:25
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.

2 participants