Skip to content

✨ Add option to enable brand settings#2319

Closed
lukevella wants to merge 2 commits intomainfrom
luke/ral-1128-branding-settings
Closed

✨ Add option to enable brand settings#2319
lukevella wants to merge 2 commits intomainfrom
luke/ral-1128-branding-settings

Conversation

@lukevella
Copy link
Copy Markdown
Owner

@lukevella lukevella commented Apr 8, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added custom branding capability allowing spaces to personalize polls with custom colors and logos.
    • Introduced color picker tool for selecting custom brand colors.
    • Added branding upsell prompt for free-tier users.
  • UI & Styling

    • Enhanced form components with improved field layouts.
    • Updated alert component with action slot support.
    • Refined card and button styling across the app.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 8, 2026

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

Project Deployment Actions Updated (UTC)
app Ready Ready Preview, Comment Apr 8, 2026 4:37pm
landing Ready Ready Preview, Comment Apr 8, 2026 4:37pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

Walkthrough

This PR implements custom branding functionality for spaces, allowing users to enable branding display and configure primary colors through new settings UI components. Changes include localization strings, branding-gated UI components with paywall integration, EventCard prop refactoring for branding configuration, TRPC route consolidation, and new UI primitives (ColorPicker, Field components, AlertAction).

Changes

Cohort / File(s) Summary
Localization
apps/web/public/locales/en/app.json
Added custom branding i18n keys (alert titles, descriptions, button labels, status states) and removed deprecated image key.
Poll/Invite Branding Rendering
apps/web/src/app/[locale]/(optional-space)/poll/[urlId]/admin-page.tsx, apps/web/src/app/[locale]/invite/[urlId]/invite-page.tsx, apps/web/src/components/event-card.tsx
Refactored EventCard to accept name and logoUrl props; updated admin and invite pages to conditionally pass branding data based on poll.space?.showBranding.
Custom Branding Alert
apps/web/src/app/[locale]/(optional-space)/poll/[urlId]/custom-branding-alert.tsx
New component displaying upsell alert to free-tier users when branding is disabled; triggers paywall on button click.
Space Settings - Branding UI
apps/web/src/app/[locale]/(space)/settings/general/components/custom-branding-section.tsx, apps/web/src/app/[locale]/(space)/settings/general/components/custom-branding-switch.tsx, apps/web/src/app/[locale]/(space)/settings/general/components/space-branding-form.tsx
New components for managing branding: toggle switch (with paywall gating), color picker form, and section wrapper; all integrate TRPC mutations with toast feedback and cache invalidation.
Space Settings - Form Migration
apps/web/src/app/[locale]/(space)/settings/general/components/space-settings-form.tsx
Migrated form from @rallly/ui/form to @rallly/ui/field primitives; replaced FormField render-props with Controller-based React Hook Form integration.
Settings Page Integration
apps/web/src/app/[locale]/(space)/settings/general/page-client.tsx, apps/web/src/app/[locale]/(space)/settings/general/page.tsx
Added CustomBrandingSection import and render; converted GeneralSettingsPage to async function signature.
Space Branding Provider Logic
apps/web/src/features/space/client.tsx
Updated SpaceProvider to gate primaryColorVars rendering on both space.showBranding and space.primaryColor being truthy.
Paywall Integration
apps/web/src/features/billing/components/pay-wall-dialog.tsx
Added custom branding as a Pro plan benefit with localized title/description and PaletteIcon.
TRPC Routes
apps/web/src/trpc/routers/spaces.ts
Consolidated spaces.update mutation to accept optional primaryColor field; removed dedicated spaces.updatePrimaryColor mutation.
UI Components - ColorPicker
packages/ui/src/color-picker.tsx
New color picker component built on react-aria-components with hex input, saturation/brightness area, and hue slider.
UI Components - Field Primitives
packages/ui/src/field.tsx
New composable form field component system (FieldSet, FieldGroup, Field, FieldLabel, FieldError, etc.) replacing legacy form patterns.
UI Component Updates
packages/ui/src/alert.tsx, packages/ui/src/button-variants.ts, packages/ui/src/switch.tsx, packages/ui/src/sidebar.tsx, packages/ui/src/pro-badge.tsx, packages/ui/src/stacked-list.tsx
Minor styling/variant updates: AlertAction export addition, button active-state modifier removal, switch opacity change, sidebar border addition, badge size prop, and list background opacity adjustment.
Tailwind Shared Styles
packages/tailwind-config/shared-styles.css
Changed default primary color fallback from indigo-600 to gray-800; simplified card and card-border from color-mix blends to solid color assignments.
Dependencies
packages/ui/package.json
Added react-aria-components, @react-aria/color, and @react-stately/color for color picker functionality.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant SettingsUI as Settings Page
    participant Switch as CustomBrandingSwitch
    participant Billing as useBilling()
    participant TRPC as TRPC Mutation
    participant Cache as Query Cache
    participant DB as Database

    User->>SettingsUI: Navigate to branding settings
    User->>Switch: Toggle "Show Branding"
    Switch->>Billing: Check plan (isFree)
    alt Free Plan
        Billing-->>Switch: isFree = true
        Switch->>User: Trigger showPayWall()
    else Pro Plan
        Billing-->>Switch: isFree = false
        Switch->>TRPC: updateShowBranding({ showBranding: true })
        TRPC->>DB: Update space.showBranding
        DB-->>TRPC: Success
        TRPC->>Cache: Invalidate spaces.getCurrent
        Cache-->>SettingsUI: Refetch space data
        Switch->>User: Show success toast
    end
Loading
sequenceDiagram
    actor User
    participant AdminPage as Admin Page
    participant Alert as CustomBrandingAlert
    participant Billing as useBilling()
    participant Dialog as PayWallDialog

    User->>AdminPage: View poll admin page
    AdminPage->>Alert: Render CustomBrandingAlert
    Alert->>Billing: Check plan
    alt Free Plan + Branding Disabled
        Billing-->>Alert: Show alert
        Alert->>User: Display upsell alert with palette icon
        User->>Alert: Click "Upgrade" button
        Alert->>Dialog: Trigger showPayWall()
        Dialog->>User: Show upgrade dialog with branding benefit
    else Other Conditions
        Alert-->>AdminPage: Return null (no render)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🎨 A painter rabbit hops with glee,
Custom colors for all to see!
Purple, gold, or shades so fine—
Brands now blossom, pixel by design! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add option to enable brand settings' accurately describes the main change: introducing new UI components and settings for enabling/configuring custom branding features across the application.

✏️ 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 luke/ral-1128-branding-settings

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
Copy Markdown
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

🧹 Nitpick comments (4)
apps/web/src/app/[locale]/(space)/settings/general/page.tsx (1)

5-6: Drop unnecessary async from GeneralSettingsPage (Line 5).

No awaited work is performed in this function.

♻️ Suggested simplification
-export default async function GeneralSettingsPage() {
+export default function GeneralSettingsPage() {
   return <GeneralSettingsPageClient />;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/`[locale]/(space)/settings/general/page.tsx around lines 5 -
6, Remove the unnecessary async from the GeneralSettingsPage function
declaration: update the export default function GeneralSettingsPage() to be a
plain synchronous function that simply returns <GeneralSettingsPageClient /> (no
await usage), keeping the component name GeneralSettingsPage and the client
component GeneralSettingsPageClient unchanged.
packages/ui/src/field.tsx (2)

5-5: Use React.useMemo instead of standalone import.

Per coding guidelines, prefer React.useMemo over standalone useMemo import.

♻️ Proposed fix
-import { useMemo } from "react";
+import React from "react";

Then at line 185:

-  const content = useMemo(() => {
+  const content = React.useMemo(() => {

As per coding guidelines: "Prefer React.useState, React.useEffect, etc. over standalone imports (useState, useEffect)"

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

In `@packages/ui/src/field.tsx` at line 5, The file imports useMemo directly;
update to use React.useMemo per guidelines by replacing the standalone import
(remove useMemo from the named imports) and ensure all calls to useMemo (e.g.,
inside the Field component or any function where useMemo is referenced at/around
the existing usage) are changed to React.useMemo; locate references by the
symbol useMemo and the Field component or the block around line ~185 and switch
them to React.useMemo after importing React as the default.

78-79: Minor typo in biome-ignore comments.

"defualt" should be "default" in the biome-ignore comments at lines 78 and 206.

✏️ Proposed fix
-    // biome-ignore lint/a11y/useSemanticElements: avoid messing with shadcn defualt implementation
+    // biome-ignore lint/a11y/useSemanticElements: avoid messing with shadcn default implementation
-          // biome-ignore lint/suspicious/noArrayIndexKey: avoid messing with shadcn defualt implementation
+          // biome-ignore lint/suspicious/noArrayIndexKey: avoid messing with shadcn default implementation

Also applies to: 206-207

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

In `@packages/ui/src/field.tsx` around lines 78 - 79, Fix the typo in the
biome-ignore comments: change "defualt" to "default" in the comment immediately
before the <div> element (the biome-ignore lint/a11y/useSemanticElements
comment) and the matching comment later in the file (the second biome-ignore
lint/a11y/useSemanticElements instance near the other <div>), ensuring both
comments read "avoid messing with shadcn default implementation".
apps/web/src/app/[locale]/(space)/settings/general/components/custom-branding-section.tsx (1)

74-96: Consider explicit disabled state on Button.

The Button at line 87-93 relies on pointer-events-none from the parent div when branding is disabled. For better semantics and accessibility, consider explicitly disabling the button:

♻️ Suggested change
               <Button
                 onClick={handleSave}
-                disabled={!isDirty}
+                disabled={!space.showBranding || !isDirty}
                 loading={updateSpace.isPending}
               >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/src/app/`[locale]/(space)/settings/general/components/custom-branding-section.tsx
around lines 74 - 96, The Button relies on the parent div's pointer-events for
disabling which is not semantic or accessible; update the Button in the Custom
Branding section to explicitly set disabled when branding is turned off (use
space.showBranding to compute the prop), keep the existing disabled={!isDirty}
behavior combined (e.g. disabled={!space.showBranding || !isDirty}), preserve
loading={updateSpace.isPending}, and add an aria-disabled attribute if the
Button component supports it so screen readers get the correct state; ensure
handleSave remains the click handler but will not run when the explicit disabled
is true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/web/src/app/`[locale]/(space)/settings/general/components/custom-branding-switch.tsx:
- Around line 21-41: The onChange callback in handleToggle fires immediately
instead of after the mutation settles, causing possible UI-server state
mismatch; move the onChange?.(checked) call into the success path of the
mutation (e.g., chain it inside the promise .then() after
updateShowBranding.mutateAsync and utils.spaces.getCurrent.invalidate()), or
remove the call entirely and rely on utils.spaces.getCurrent.invalidate() to
refresh state—update handleToggle to only invoke onChange after the mutation and
invalidation complete.

In
`@apps/web/src/app/`[locale]/(space)/settings/general/components/space-settings-form.tsx:
- Around line 66-85: The form is being reset immediately because
form.reset(data) is called right after starting toast.promise; change the flow
so the reset happens only after updateSpace.mutateAsync succeeds: inside the
onSubmit handler (wrapped by form.handleSubmit) await updateSpace.mutateAsync({
name: data.name }) (or use the Promise returned by
updateSpace.mutateAsync/.then) and call form.reset(data) in the success path,
keeping toast.promise around the same mutation promise to show UI notifications;
ensure form.reset is not executed on mutation failure.

In `@apps/web/src/app/`[locale]/(space)/settings/general/page-client.tsx:
- Line 68: CustomBrandingSection is currently rendered without admin gating so
non-admins can interact then hit FORBIDDEN; update the usage and component to
mirror SpaceSettingsForm's pattern by passing a disabled prop (e.g.,
disabled={!isAdmin}) where <CustomBrandingSection /> is rendered and update
CustomBrandingSection to respect that prop by disabling the color picker and
save button when disabled is true (preserve backend checks in spaces.update and
spaces.updateShowBranding). Ensure the prop name matches how SpaceSettingsForm
uses it (disabled) and that handlers early-return or UI controls are
uninteractive when disabled.

In `@apps/web/src/components/event-card.tsx`:
- Around line 59-63: The branding block currently renders only when logoUrl is
truthy, hiding the space name if branding is enabled but no image exists; update
the JSX so that when showBranding (or the prop controlling branding) is true the
space name (variable name) is always rendered and SpaceIcon (component
SpaceIcon) is shown only if logoUrl (derived from image) exists — i.e., move the
<p> that displays name out of the logoUrl conditional (or add a fallback branch)
so name appears even when image/logoUrl is undefined.

---

Nitpick comments:
In
`@apps/web/src/app/`[locale]/(space)/settings/general/components/custom-branding-section.tsx:
- Around line 74-96: The Button relies on the parent div's pointer-events for
disabling which is not semantic or accessible; update the Button in the Custom
Branding section to explicitly set disabled when branding is turned off (use
space.showBranding to compute the prop), keep the existing disabled={!isDirty}
behavior combined (e.g. disabled={!space.showBranding || !isDirty}), preserve
loading={updateSpace.isPending}, and add an aria-disabled attribute if the
Button component supports it so screen readers get the correct state; ensure
handleSave remains the click handler but will not run when the explicit disabled
is true.

In `@apps/web/src/app/`[locale]/(space)/settings/general/page.tsx:
- Around line 5-6: Remove the unnecessary async from the GeneralSettingsPage
function declaration: update the export default function GeneralSettingsPage()
to be a plain synchronous function that simply returns
<GeneralSettingsPageClient /> (no await usage), keeping the component name
GeneralSettingsPage and the client component GeneralSettingsPageClient
unchanged.

In `@packages/ui/src/field.tsx`:
- Line 5: The file imports useMemo directly; update to use React.useMemo per
guidelines by replacing the standalone import (remove useMemo from the named
imports) and ensure all calls to useMemo (e.g., inside the Field component or
any function where useMemo is referenced at/around the existing usage) are
changed to React.useMemo; locate references by the symbol useMemo and the Field
component or the block around line ~185 and switch them to React.useMemo after
importing React as the default.
- Around line 78-79: Fix the typo in the biome-ignore comments: change "defualt"
to "default" in the comment immediately before the <div> element (the
biome-ignore lint/a11y/useSemanticElements comment) and the matching comment
later in the file (the second biome-ignore lint/a11y/useSemanticElements
instance near the other <div>), ensuring both comments read "avoid messing with
shadcn default implementation".
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 79ee3fef-04a4-48ec-86d6-4ed6d5091b50

📥 Commits

Reviewing files that changed from the base of the PR and between 156041a and dd48266.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (24)
  • apps/web/public/locales/en/app.json
  • apps/web/src/app/[locale]/(optional-space)/poll/[urlId]/admin-page.tsx
  • apps/web/src/app/[locale]/(optional-space)/poll/[urlId]/custom-branding-alert.tsx
  • apps/web/src/app/[locale]/(space)/settings/general/components/custom-branding-section.tsx
  • apps/web/src/app/[locale]/(space)/settings/general/components/custom-branding-switch.tsx
  • apps/web/src/app/[locale]/(space)/settings/general/components/space-branding-form.tsx
  • apps/web/src/app/[locale]/(space)/settings/general/components/space-settings-form.tsx
  • apps/web/src/app/[locale]/(space)/settings/general/page-client.tsx
  • apps/web/src/app/[locale]/(space)/settings/general/page.tsx
  • apps/web/src/app/[locale]/invite/[urlId]/invite-page.tsx
  • apps/web/src/components/event-card.tsx
  • apps/web/src/components/pro-badge.tsx
  • apps/web/src/components/stacked-list.tsx
  • apps/web/src/features/billing/components/pay-wall-dialog.tsx
  • apps/web/src/features/space/client.tsx
  • apps/web/src/trpc/routers/spaces.ts
  • packages/tailwind-config/shared-styles.css
  • packages/ui/package.json
  • packages/ui/src/alert.tsx
  • packages/ui/src/button-variants.ts
  • packages/ui/src/color-picker.tsx
  • packages/ui/src/field.tsx
  • packages/ui/src/sidebar.tsx
  • packages/ui/src/switch.tsx

Comment on lines +21 to +41
const handleToggle = (checked: boolean) => {
if (isFree) {
showPayWall();
return;
}
toast.promise(
updateShowBranding
.mutateAsync({ showBranding: checked })
.then(() => utils.spaces.getCurrent.invalidate()),
{
loading: t("savingBranding", { defaultValue: "Saving..." }),
success: checked
? t("brandingEnabled", { defaultValue: "Custom branding enabled" })
: t("brandingDisabled", { defaultValue: "Custom branding disabled" }),
error: t("brandingSaveError", {
defaultValue: "Failed to save branding",
}),
},
);
onChange?.(checked);
};
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.

⚠️ Potential issue | 🟡 Minor

onChange callback fires before mutation completes.

onChange?.(checked) on line 40 executes immediately after starting the mutation, not after it succeeds. If the mutation fails, the parent component's state may be inconsistent with the server state.

Consider moving onChange into the .then() chain or removing it entirely since utils.spaces.getCurrent.invalidate() already refreshes the data.

🐛 Proposed fix
   const handleToggle = (checked: boolean) => {
     if (isFree) {
       showPayWall();
       return;
     }
     toast.promise(
       updateShowBranding
         .mutateAsync({ showBranding: checked })
-        .then(() => utils.spaces.getCurrent.invalidate()),
+        .then(() => {
+          utils.spaces.getCurrent.invalidate();
+          onChange?.(checked);
+        }),
       {
         loading: t("savingBranding", { defaultValue: "Saving..." }),
         success: checked
           ? t("brandingEnabled", { defaultValue: "Custom branding enabled" })
           : t("brandingDisabled", { defaultValue: "Custom branding disabled" }),
         error: t("brandingSaveError", {
           defaultValue: "Failed to save branding",
         }),
       },
     );
-    onChange?.(checked);
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/src/app/`[locale]/(space)/settings/general/components/custom-branding-switch.tsx
around lines 21 - 41, The onChange callback in handleToggle fires immediately
instead of after the mutation settles, causing possible UI-server state
mismatch; move the onChange?.(checked) call into the success path of the
mutation (e.g., chain it inside the promise .then() after
updateShowBranding.mutateAsync and utils.spaces.getCurrent.invalidate()), or
remove the call entirely and rely on utils.spaces.getCurrent.invalidate() to
refresh state—update handleToggle to only invoke onChange after the mutation and
invalidation complete.

Comment on lines +66 to +85
onSubmit={form.handleSubmit(async (data) => {
toast.promise(
updateSpace.mutateAsync({
name: data.name,
}),
{
loading: t("updatingSpace", {
defaultValue: "Updating space...",
}),
{
loading: t("updatingSpace", {
defaultValue: "Updating space...",
}),
success: t("spaceUpdatedSuccess", {
defaultValue: "Space updated successfully",
}),
error: t("spaceUpdatedError", {
defaultValue: "Failed to update space",
}),
},
);
success: t("spaceUpdatedSuccess", {
defaultValue: "Space updated successfully",
}),
error: t("spaceUpdatedError", {
defaultValue: "Failed to update space",
}),
},
);

form.reset(data);
})}
className="space-y-6"
>
<div>
<FormLabel>
<Trans i18nKey="image" defaults="Image" />
</FormLabel>
<ImageUpload className="mt-2">
<ImageUploadPreview>
<SpaceIcon name={space.name} src={space.image} size="xl" />
</ImageUploadPreview>
<ImageUploadControl
getUploadUrl={(input) => getImageUploadUrl.mutateAsync(input)}
onUploadSuccess={handleImageUploadSuccess}
onRemoveSuccess={handleImageRemoveSuccess}
hasCurrentImage={!!space.image}
/>
</ImageUpload>
</div>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey="name" defaults="Name" />
</FormLabel>
<FormControl>
<Input
className="w-72"
placeholder={t("spaceNamePlaceholder", {
defaultValue: "My Team",
})}
disabled={disabled}
{...field}
form.reset(data);
})}
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.

⚠️ Potential issue | 🟠 Major

form.reset(data) executes before mutation completes.

form.reset(data) on line 84 runs immediately after calling toast.promise, not after the mutation succeeds. If the mutation fails, the form state will already be reset, losing the user's changes.

🐛 Proposed fix: move reset into promise chain
       onSubmit={form.handleSubmit(async (data) => {
         toast.promise(
           updateSpace.mutateAsync({
             name: data.name,
-          }),
+          }).then(() => {
+            form.reset(data);
+          }),
           {
             loading: t("updatingSpace", {
               defaultValue: "Updating space...",
             }),
             success: t("spaceUpdatedSuccess", {
               defaultValue: "Space updated successfully",
             }),
             error: t("spaceUpdatedError", {
               defaultValue: "Failed to update space",
             }),
           },
         );
-
-        form.reset(data);
       })}
📝 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
onSubmit={form.handleSubmit(async (data) => {
toast.promise(
updateSpace.mutateAsync({
name: data.name,
}),
{
loading: t("updatingSpace", {
defaultValue: "Updating space...",
}),
{
loading: t("updatingSpace", {
defaultValue: "Updating space...",
}),
success: t("spaceUpdatedSuccess", {
defaultValue: "Space updated successfully",
}),
error: t("spaceUpdatedError", {
defaultValue: "Failed to update space",
}),
},
);
success: t("spaceUpdatedSuccess", {
defaultValue: "Space updated successfully",
}),
error: t("spaceUpdatedError", {
defaultValue: "Failed to update space",
}),
},
);
form.reset(data);
})}
className="space-y-6"
>
<div>
<FormLabel>
<Trans i18nKey="image" defaults="Image" />
</FormLabel>
<ImageUpload className="mt-2">
<ImageUploadPreview>
<SpaceIcon name={space.name} src={space.image} size="xl" />
</ImageUploadPreview>
<ImageUploadControl
getUploadUrl={(input) => getImageUploadUrl.mutateAsync(input)}
onUploadSuccess={handleImageUploadSuccess}
onRemoveSuccess={handleImageRemoveSuccess}
hasCurrentImage={!!space.image}
/>
</ImageUpload>
</div>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey="name" defaults="Name" />
</FormLabel>
<FormControl>
<Input
className="w-72"
placeholder={t("spaceNamePlaceholder", {
defaultValue: "My Team",
})}
disabled={disabled}
{...field}
form.reset(data);
})}
onSubmit={form.handleSubmit(async (data) => {
toast.promise(
updateSpace.mutateAsync({
name: data.name,
}).then(() => {
form.reset(data);
}),
{
loading: t("updatingSpace", {
defaultValue: "Updating space...",
}),
success: t("spaceUpdatedSuccess", {
defaultValue: "Space updated successfully",
}),
error: t("spaceUpdatedError", {
defaultValue: "Failed to update space",
}),
},
);
})}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/src/app/`[locale]/(space)/settings/general/components/space-settings-form.tsx
around lines 66 - 85, The form is being reset immediately because
form.reset(data) is called right after starting toast.promise; change the flow
so the reset happens only after updateSpace.mutateAsync succeeds: inside the
onSubmit handler (wrapped by form.handleSubmit) await updateSpace.mutateAsync({
name: data.name }) (or use the Promise returned by
updateSpace.mutateAsync/.then) and call form.reset(data) in the success path,
keeping toast.promise around the same mutation promise to show UI notifications;
ensure form.reset is not executed on mutation failure.

<SpaceSettingsForm space={space} disabled={!isAdmin} />
</PageSectionContent>
</PageSection>
<CustomBrandingSection />
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Locate custom branding settings components"
fd -i "custom-branding.*\\.tsx$" apps/web/src

echo
echo "2) Inspect custom branding section implementation"
fd -i "custom-branding-section.tsx" apps/web/src -0 | xargs -0 -I{} sh -c 'echo "### {}"; sed -n "1,260p" "{}"'

echo
echo "3) Inspect branding mutations / authorization checks"
rg -n -C3 --type=ts --type=tsx '\bshowBranding\b|\bprimaryColor\b|\bisAdmin\b|\bability\b|\bcan\(|\btrpc\b|\bmutation\b|\bupdate\b' apps/web/src

Repository: lukevella/rallly

Length of output: 4041


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Check parent page-client.tsx to see how CustomBrandingSection is rendered"
sed -n '60,80p' apps/web/src/app/[locale]\(space\)/settings/general/page-client.tsx

echo
echo "2) Check if there's admin gating at page level"
grep -n "isAdmin\|CustomBrandingSection" apps/web/src/app/[locale]\(space\)/settings/general/page-client.tsx

echo
echo "3) Check trpc spaces.update mutation handler for authorization"
fd "spaces" apps/web/src -type f -name "*.ts" | xargs grep -l "\.update" | head -5

echo
echo "4) Search for space router/procedure definitions"
find apps/web/src -name "*router*" -o -name "*procedure*" | grep -i space | head -10

Repository: lukevella/rallly

Length of output: 262


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Find the page-client file"
find apps/web/src -name "page-client.tsx" | grep "settings/general"

echo
echo "2) Read the page-client file around the CustomBrandingSection usage"
FILE=$(find apps/web/src -name "page-client.tsx" | grep "settings/general")
if [ -n "$FILE" ]; then
  cat -n "$FILE" | sed -n '50,85p'
fi

echo
echo "3) Check for conditional rendering of CustomBrandingSection"
if [ -n "$FILE" ]; then
  grep -n "CustomBrandingSection\|isAdmin" "$FILE"
fi

echo
echo "4) Find server-side trpc router for spaces"
find apps -name "*router*" -type f | xargs grep -l "spaces" | grep -E "\.(ts|tsx)$" | head -3

Repository: lukevella/rallly

Length of output: 2264


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Find trpc router files"
find apps -type d -name "router" | grep -E "server|api" | head -5

echo
echo "2) Find spaces router"
fd ".*router.*" apps -type f -name "*.ts" | xargs grep -l "spaces" 2>/dev/null | head -5

echo
echo "3) Find spaces procedure or router definition"
find apps -type f -name "*.ts" | xargs grep -l "spaces.*Router\|export.*spaces" 2>/dev/null | head -5

Repository: lukevella/rallly

Length of output: 85


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) List directory structure to understand trpc organization"
find apps/web/src -type d -name "trpc" -o -type d -name "server"

echo
echo "2) Find all files related to spaces in server context"
find apps -type f \( -name "*.ts" -o -name "*.tsx" \) | xargs grep -l "spaces.*update\|spaces\.Router" 2>/dev/null | head -10

echo
echo "3) Search for procedures or routers with 'update' in spaces context"
rg "spaces\." apps -t ts -A 5 | grep -A 5 "update" | head -40

echo
echo "4) Find where mutation handlers check authorization"
rg "isAdmin\|role.*admin\|ability" apps -t ts --context=3 | grep -B 3 -A 3 "update" | head -50

Repository: lukevella/rallly

Length of output: 4709


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) List files in trpc/server"
find apps/web/src/trpc/server -type f -name "*.ts" | head -20

echo
echo "2) Look for spaces router"
find apps/web/src/trpc -type f -name "*.ts" | xargs grep -l "spaces" | head -10

echo
echo "3) Check the main router structure"
cat apps/web/src/trpc/server/routers/_app.ts 2>/dev/null | head -50

Repository: lukevella/rallly

Length of output: 268


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Read spaces router to find update procedure"
cat apps/web/src/trpc/routers/spaces.ts | head -200

echo
echo "2) Look for update procedure specifically"
grep -n "\.update\|export const\|procedure" apps/web/src/trpc/routers/spaces.ts | head -30

Repository: lukevella/rallly

Length of output: 5604


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Find the update procedure definition"
grep -n "update:" apps/web/src/trpc/routers/spaces.ts

echo
echo "2) Read the update procedure around line 280-350"
sed -n '280,350p' apps/web/src/trpc/routers/spaces.ts

echo
echo "3) Check what spaceProcedure is and if it has authorization"
grep -n "spaceProcedure\|export.*spaceProcedure" apps/web/src/trpc/trpc.ts

Repository: lukevella/rallly

Length of output: 2479


Add admin-gating UI to CustomBrandingSection for consistency.

Backend authorization is properly enforced via memberAbility checks in spaces.update and spaces.updateShowBranding mutations. However, non-admins can still see and interact with this section (color picker, save button) only to receive a FORBIDDEN error on save. For UX consistency with SpaceSettingsForm which displays disabled={!isAdmin}, add the same admin-gating pattern to CustomBrandingSection to prevent unnecessary interaction and error states.

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

In `@apps/web/src/app/`[locale]/(space)/settings/general/page-client.tsx at line
68, CustomBrandingSection is currently rendered without admin gating so
non-admins can interact then hit FORBIDDEN; update the usage and component to
mirror SpaceSettingsForm's pattern by passing a disabled prop (e.g.,
disabled={!isAdmin}) where <CustomBrandingSection /> is rendered and update
CustomBrandingSection to respect that prop by disabling the color picker and
save button when disabled is true (preserve backend checks in spaces.update and
spaces.updateShowBranding). Ensure the prop name matches how SpaceSettingsForm
uses it (disabled) and that handlers early-return or UI controls are
uninteractive when disabled.

Comment on lines +59 to +63
{logoUrl ? (
<div className="mb-2">
<SpaceIcon
name={poll.space.name}
src={poll.space.image}
size="lg"
/>
<SpaceIcon name={name} src={logoUrl} size="lg" />
<p className="mt-2 font-medium text-muted-foreground text-sm">
{poll.space.name}
{name}
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Inspect EventCard branding branch"
sed -n '40,120p' apps/web/src/components/event-card.tsx

echo
echo "2) Inspect EventCard callsites and showBranding gating"
rg -n -C3 --type=tsx '\bEventCard\b|\bshowBranding\b|\blogoUrl\b|\bimage\b' apps/web/src/app

echo
echo "3) Inspect whether space image/logo is optional in schema/types"
rg -n -C3 --type=ts '\bimage\b|\blogo\b|\bshowBranding\b' apps/web/src/features/space

Repository: lukevella/rallly

Length of output: 2359


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== 1) EventCard callsites - search all TypeScript/TSX files ==="
rg -n -C3 'EventCard' apps/web/src/app --type ts --type tsx || rg -n -C3 'EventCard' apps/web/src/app

echo
echo "=== 2) Space type definitions and logoUrl/image field ==="
fd -e ts -e tsx apps/web/src/features/space -o -type f | head -10
echo "---"
rg -n 'interface.*Space|type.*Space|image|logo' apps/web/src/features/space -A3 -B1

echo
echo "=== 3) Check if showBranding is used elsewhere ==="
rg -n 'showBranding|logo' apps/web/src --type ts --type tsx | head -20

Repository: lukevella/rallly

Length of output: 3287


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== EventCard callsite 1: invite-page.tsx ==="
sed -n '60,70p' apps/web/src/app/[locale]/invite/[urlId]/invite-page.tsx

echo
echo "=== EventCard callsite 2: admin-page.tsx ==="
sed -n '26,36p' apps/web/src/app/[locale]/\(optional-space\)/poll/[urlId]/admin-page.tsx

echo
echo "=== Search for Space type definition ==="
rg -n 'interface Space|type Space' apps/web/src/features/space --max-count 5

echo
echo "=== Search for showBranding and logo/image fields ==="
rg -n 'showBranding' apps/web/src --max-count 20

Repository: lukevella/rallly

Length of output: 3920


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Space type definition ==="
cat -n apps/web/src/features/space/types.ts

echo
echo "=== Check Prisma schema for Space model image field ==="
rg -n 'model Space|image' apps/web --max-count 30 -B2 -A5 | grep -E '(model Space|image|String|Boolean)' | head -20

Repository: lukevella/rallly

Length of output: 2759


Consider showing space name independently of logo upload.

The branding section only renders when logoUrl is truthy, which requires both showBranding: true AND an uploaded image. This means a space with branding enabled but no logo uploaded will display no branding header at all. The name should display even when the logo is missing to maintain UX clarity.

Suggested fix: render the space name outside the logo condition, or create a separate fallback when showBranding is true but image is undefined.

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

In `@apps/web/src/components/event-card.tsx` around lines 59 - 63, The branding
block currently renders only when logoUrl is truthy, hiding the space name if
branding is enabled but no image exists; update the JSX so that when
showBranding (or the prop controlling branding) is true the space name (variable
name) is always rendered and SpaceIcon (component SpaceIcon) is shown only if
logoUrl (derived from image) exists — i.e., move the <p> that displays name out
of the logoUrl conditional (or add a fallback branch) so name appears even when
image/logoUrl is undefined.

@lukevella lukevella closed this Apr 9, 2026
@lukevella lukevella deleted the luke/ral-1128-branding-settings branch April 9, 2026 09:47
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