Organization Switcher#165
Conversation
- Added `OrgMenu` component for organization switching in the sidebar. - Introduced `CreateOrgModal` for creating new organizations with form validation. - Updated `changeOrganizationAction` to revalidate paths after organization change. - Enhanced `OrganizationSwitcher` to handle organization changes with loading states and user feedback. - Improved header and sidebar components for better organization management experience. - Updated translations to include new status messages for organization switching.
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
WalkthroughThis PR introduces new functionality and UI enhancements related to organization management. The changes add the revalidation of organization pages upon update, incorporate new modal and menu components for creating and selecting organizations, and refactor existing components to improve user interaction. There are also several localization updates and minor code formatting adjustments across various files. Changes
Sequence Diagram(s)Organization Switch FlowsequenceDiagram
actor User
participant OS as OrganizationSwitcher (Dialog)
participant Action as changeOrganizationAction
participant Cache as revalidatePath
participant Nav as Navigation
User->>OS: Select an organization
OS->>OS: Set loading state and open dialog
OS->>Action: Trigger organization change action
Action->>Cache: Call revalidatePath(orgId)
Cache-->>Action: Path revalidated
Action-->>OS: Return success
OS->>Nav: Redirect to new organization page after delay
Nav-->>User: New organization page displayed
Organization Creation FlowsequenceDiagram
actor User
participant Modal as CreateOrgModal
participant Action as createOrganization
participant Status as RealtimeStatus
participant Nav as Navigation
User->>Modal: Open and fill organization form
Modal->>Action: Submit form with org details
Action->>Status: Monitor creation status
Status-->>Modal: Provide real-time updates
Action-->>Modal: Return new organization details and token
Modal->>Nav: Redirect user to new organization page
Nav-->>User: New organization page displayed
Possibly related PRs
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (11)
packages/ui/src/components/chart.tsx (1)
182-182: Added z-index to tooltip containerA
z-30class has been added to the tooltip container's className, which will affect the tooltip's stacking order relative to other elements.Make sure this z-index value (30) is appropriate for your application's overall stacking context. If tooltips need to appear above other high z-index elements, you might need to increase this value.
apps/app/src/components/sidebar.tsx (3)
8-9: Confirm the fallback handling for Suspense.
By importingSuspenseandOrgMenu, you're setting up asynchronous rendering in React, but there is no explicit fallback element provided. This may lead to a blank screen if data fetching is delayed. Consider providing a small spinner or skeleton UI for better user feedback.
22-24: Handle potential JSON parsing errors.
When retrieving theMenuConfigcookie, you assume valid JSON. Malformed JSON would cause a runtime error. WrapJSON.parsein a try/catch for more robust error handling.const initialItems = cookieStore.has(Cookies.MenuConfig) ? JSON.parse(cookieStore.get(Cookies.MenuConfig)?.value ?? "") : null; +// Potential safety net for malformed cookies: +let parsedMenuConfig = null; +try { + parsedMenuConfig = cookieStore.has(Cookies.MenuConfig) + ? JSON.parse(cookieStore.get(Cookies.MenuConfig)?.value ?? "") + : null; +} catch (e) { + console.error("Invalid JSON in MenuConfig cookie:", e); +} +const initialItems = parsedMenuConfig;
26-43: Add a fallback for OrgMenu Suspense.
The return block includes<Suspense>without specifying a fallback within the component. Users may see an empty area during the async fetch. Provide a lightweight fallback UI or skeleton to improve overall user experience.apps/app/src/components/modals/create-org-modal.tsx (1)
240-281: RealtimeStatus component handles asynchronous completion well.
TheRealtimeStatuslogic elegantly transitions to the organization page when the run is successfully completed. Minor suggestions:
- Log or expose the
errormore prominently in the UI if you detect one, so users know the creation has failed.- Consider ensuring the final redirect only happens once to avoid potential race conditions if re-render triggers
useEffectmultiple times.
Overall, this chunk is appropriately designed for smooth user feedback.apps/app/src/components/organization-switcher.tsx (6)
4-7: Consolidate new imports.
Imports forFramework,Organization,Dialog,motion,useClickAway, etc., have increased. This is acceptable given new features, but watch out for potential bloat. Remove any imports that are no longer needed to keep the file concise.Also applies to: 10-18
20-24: OrgItem interface clarity.
YourOrgIteminterface provides a simple shape for each org. If you plan to store more metadata, consider a partial or extended type from the originalOrganizationmodel for consistency.
37-41: Manage state for dialog, active status, and loading.
Using separate statesisOpen,isActive, andisLoadingclarifies the user flow. Consider grouping them if they share a life cycle. This is optional but can sometimes simplify logic.
43-65: Asynchronous organization change with delayed navigation.
- Good handling of success/failure states, including a 1.5-second delay for user experience.
- For large user bases or remote connections, consider confirming success before applying a forced delay to avoid possible confusion.
- The
router.refresh()ensures partial revalidation. Confirm it doesn’t over-fetch or conflict with other revalidation logic.
72-75: Ensure error handling around handleOrganizationChange.
You setisLoadingand then triggerexecuteto change org. Ifexecutefails, you reset loading inonError. Looks fine. Possibly add final success state reset if you want the loading state to end after the final navigation.
83-121: Portal-based loading overlay.
Blocking the UI and blurring the background is a good pattern for switching contexts. However, you might consider giving the user a cancellation path if the switch is misclicked or slow.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
apps/app/languine.lockis excluded by!**/*.lock
📒 Files selected for processing (15)
apps/app/src/actions/change-organization.ts(1 hunks)apps/app/src/components/header.tsx(1 hunks)apps/app/src/components/modals/create-org-modal.tsx(1 hunks)apps/app/src/components/org-menu.tsx(1 hunks)apps/app/src/components/organization-switcher.tsx(1 hunks)apps/app/src/components/sidebar.tsx(1 hunks)apps/app/src/components/ui/data-table/DataTable.tsx(2 hunks)apps/app/src/components/user-menu.tsx(0 hunks)apps/app/src/locales/en.ts(1 hunks)apps/app/src/locales/es.ts(1 hunks)apps/app/src/locales/fr.ts(1 hunks)apps/app/src/locales/no.ts(1 hunks)apps/app/src/locales/pt.ts(1 hunks)packages/ui/src/components/chart-tooltip.tsx(2 hunks)packages/ui/src/components/chart.tsx(5 hunks)
💤 Files with no reviewable changes (1)
- apps/app/src/components/user-menu.tsx
🧰 Additional context used
🧬 Code Definitions (5)
apps/app/src/components/header.tsx (3)
apps/app/src/data/getOrganizations.ts (1) (1)
getOrganizations(4-28)apps/app/src/components/mobile-menu.tsx (1) (1)
MobileMenu(9-36)apps/app/src/components/user-menu.tsx (1) (1)
UserMenu(15-94)
apps/app/src/components/ui/data-table/DataTable.tsx (3)
apps/app/src/components/ui/data-table/DataTableHeader.tsx (1) (1)
DataTableHeader(13-62)apps/app/src/components/ui/data-table/DataTableSkeleton.tsx (1) (1)
DataTableSkeleton(21-79)apps/app/src/components/ui/data-table/DataTablePagination.tsx (1) (1)
DataTablePagination(24-85)
apps/app/src/components/sidebar.tsx (2)
apps/app/src/components/main-menu.tsx (1) (1)
MainMenu(180-402)apps/app/src/components/org-menu.tsx (1) (1)
OrgMenu(9-30)
apps/app/src/components/organization-switcher.tsx (2)
apps/app/src/actions/change-organization.ts (1) (1)
changeOrganizationAction(8-76)apps/app/src/components/modals/create-org-modal.tsx (1) (1)
CreateOrgModal(37-238)
apps/app/src/components/org-menu.tsx (1)
apps/app/src/components/organization-switcher.tsx (1) (1)
OrganizationSwitcher(32-205)
🔇 Additional comments (39)
apps/app/src/locales/en.ts (1)
304-304: LGTM: New localization string for organization switching.The addition of the "switch" string in the onboarding section is appropriate and consistent with the organization switching functionality described in the PR objectives.
apps/app/src/locales/es.ts (1)
289-290: LGTM: Spanish translation added correctly.The Spanish translation for the organization switching message has been added appropriately, maintaining consistency with the English version.
apps/app/src/locales/fr.ts (1)
289-290: LGTM: French translation added correctly.The French translation for the organization switching message has been added appropriately, maintaining consistency with the English and Spanish versions.
apps/app/src/locales/no.ts (1)
289-290: Properly added localization for organization switching.The addition of the "switch" key with Norwegian translation complements the existing organization-related strings. The comma added to the "creating" entry is necessary for valid object syntax.
apps/app/src/locales/pt.ts (1)
289-290: Properly added localization for organization switching.The addition of the "switch" key with Portuguese translation complements the existing organization-related strings. The comma added to the "creating" entry is necessary for valid object syntax.
packages/ui/src/components/chart-tooltip.tsx (2)
156-157: Reduced z-index for mobile tooltip consistency.The z-index has been reduced from z-60 to z-30, ensuring the tooltip doesn't appear above important UI elements like organization menus.
176-177: Standardized z-index for desktop tooltip.The z-index has been reduced from z-50 to z-30, matching the mobile tooltip's z-index value. This creates consistency in the stacking order across device types.
apps/app/src/actions/change-organization.ts (2)
6-6: Added revalidation import for UI consistency.Importing the revalidatePath function will allow proper page revalidation after organization changes.
62-62: Improved UX with path revalidation after organization change.Revalidating the path ensures that all UI components reflect the newly selected organization immediately after switching, preventing stale data and UI inconsistencies.
apps/app/src/components/org-menu.tsx (3)
1-8: Proper imports and organizationThe imports are well-organized, bringing in both local auth utilities and UI components needed for the new organization menu functionality.
9-30: Well-implemented server component with proper error handlingThe OrgMenu component is well-structured as a server component that:
- Fetches user session and organizations
- Handles the case when a user doesn't have an organization by redirecting
- Renders the OrganizationSwitcher with proper data
- Uses Suspense for loading states
32-38: Good use of caching for database queriesUsing
cachefor the database query is an excellent performance optimization that prevents redundant database calls when this component re-renders.apps/app/src/components/header.tsx (1)
28-57: Header organization improvedThe header has been streamlined by removing the OrganizationSwitcher, which has been moved to its dedicated OrgMenu component. This creates a better separation of concerns and improves component organization.
apps/app/src/components/ui/data-table/DataTable.tsx (10)
3-11: Improved import formattingThe import statements have been reformatted for better readability with consistent indentation.
20-26: Better dropdown menu imports formattingThe dropdown menu components imports have been reformatted with consistent indentation.
32-38: Consistent interface formattingThe FilterItem interface has been reformatted for better readability with consistent indentation.
40-44: Consistent FilterCategory interface formattingThe FilterCategory interface has been reformatted for better readability with consistent indentation.
46-79: Improved DataTableProps interface formattingThe DataTableProps interface has been reformatted with consistent indentation for better readability while maintaining the same structure and properties.
81-94: Consistent DataTable component parameters formattingThe DataTable function parameters have been reformatted with consistent indentation for better readability.
95-107: Well-formatted state and effect implementationsThe state management and debounced search effect implementation are now consistently formatted.
109-124: Improved table configuration formattingThe useReactTable configuration has been reformatted with consistent indentation while maintaining the same functionality.
126-129: Clear pagination display conditionThe showPagination variable declaration is now clearly formatted for better readability.
130-320: Improved JSX structure formattingThe JSX structure of the component return value has been reformatted with consistent indentation while maintaining the same layout and functionality.
packages/ui/src/components/chart.tsx (4)
87-93: Improved mapping formatting in ChartStyleThe mapping of colorConfig has been reformatted for better readability while maintaining the same logic.
106-114: Better ChartTooltipContent type definitionsThe ChartTooltipContent component's props type definition has been reformatted for improved readability and consistent indentation.
264-267: Improved ChartLegendContent type definitionsThe ChartLegendContent component's props type definition has been reformatted for improved readability and consistent indentation.
330-333: Improved condition formatting in getPayloadConfigFromPayloadThe condition for checking
payload.payloadhas been reformatted for better readability with consistent indentation.apps/app/src/components/sidebar.tsx (3)
12-14: Check for null session handling.
You rightly retrieve the session user and organization ID but rely on them without additional null checks. Ifauth()fails or returns null,userwould be undefined. Consider guarding against a null session or handle it gracefully to avoid potential runtime errors.
16-18: Ensure the redirect aligns with product requirements.
Your conditional redirect on missingorganizationIdis logical. However, verify that redirecting to/is the intended behavior (e.g., for unlinked user sessions).
20-20: Confirm security for cookie usage.
Acquiring cookies fromnext/headerslooks fine. Just verify that any sensitive data is handled correctly. If the cookie can contain PII or private tokens, ensure you sanitize or encrypt them as needed.apps/app/src/components/modals/create-org-modal.tsx (3)
1-2: Client-side rendering note.
Including"use client";ensures React client-side features are enabled, which is correct for a modal with interactive form components. No issues found.
3-36: Validate all imports and dependencies.
All imported modules (e.g.,@hookform/resolvers/zod,react-hook-form,next/navigation) appear consistent with the intended functionality. Just ensure each dependency is declared in your project’s dependencies and that versions are compatible.
37-239: Review the CreateOrgModal form logic and structure.
- The form uses
react-hook-formwith azodResolver, which is a recommended pattern for robust validation.- The
onSubmithandler callscreateOrganizationwith success and error callbacks; this is good for encapsulating side effects.- You manage
runIdandpublicAccessTokenin state to control theRealtimeStatusflow. This is a nice approach for showing progress.- Consider adding more granular error messages for distinct validation failures if needed.
- The UI is strongly typed, ensuring that each field is validated according to
organizationSchema.
Overall, the implementation is clear. Just confirm you thoroughly test edge cases (empty fields, missing frameworks, etc.).apps/app/src/components/organization-switcher.tsx (6)
27-29: New OrganizationSwitcherProps fields.
Includingframeworksaligns with the refactored structure where org creation uses the same set of frameworks. No immediate issues.
33-35: Function signature update.
Addingframeworksis consistent with the changed props above. Make sure you also pass and handle them properly whereverOrganizationSwitcheris used.
67-69: UseClickAway for better UX.
Clicking outside to close or deactivate the menu is user-friendly. This is a straightforward integration withuseClickAway. Looks good.
77-79: Find the current organization by ID.
This is a straightforward approach. No further comment needed.
81-81: Grouping organizations with the current org first.
Neat trick to sort the current org to the beginning. Just confirm the logic works whencurrentOrganizationis undefined. The filter call might fail iforgis undefined.
123-201: Dialog usage for multi-organization view and creation.
- Great use of
<CreateOrgModal>to add new organizations directly.- Toggling
isActiveandisOpenis well-structured, though you may unify them under a single mechanism if desired (one for the entire menu state).- The
framer-motionanimations add a smooth effect but watch for performance on slower devices.
…oved UX - Simplified the `OrganizationSwitcher` component by removing unnecessary animations and styling. - Enhanced `CreateOrgModal` to prevent closing during execution and conditionally render titles and descriptions based on execution state. - Updated `DialogContent` to support new props for controlling close behavior and overlay interactions.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (6)
apps/app/src/components/modals/create-org-modal.tsx (4)
49-57: Error handling could be more informative.While the success handling is comprehensive, the error handling simply shows a generic error message. Consider enhancing the error message to provide more context about what went wrong during organization creation.
onError: (error) => { - toast.error(t("common.actions.error"), { duration: 5000 }); + const errorMessage = error.serverError || t("common.actions.error"); + toast.error(errorMessage, { duration: 5000 }); },
191-232: Framework selection UI could benefit from a message when no frameworks are available.The framework selection fieldset should handle the case when no frameworks are available. Consider adding a message when the frameworks array is empty.
<fieldset className="flex flex-col gap-2 select-none"> <legend className="sr-only"> {t("frameworks.overview.grid.title")} </legend> + {frameworks.length === 0 && ( + <p className="text-sm text-muted-foreground">{t("frameworks.overview.grid.empty")}</p> + )} {frameworks.map((framework) => ( // Existing framework items ))} </fieldset>
239-241: Cancel button should reset form state.Currently, the cancel button only closes the modal. Consider resetting the form when canceling to ensure a clean state if the user reopens the modal.
<Button type="button" variant="outline" - onClick={() => onOpenChange(false)} + onClick={() => { + form.reset(); + onOpenChange(false); + }} disabled={isExecuting}> {t("common.actions.cancel")} </Button>
259-300: RealtimeStatus component handles completion correctly but lacks error recovery.The
RealtimeStatuscomponent properly redirects upon successful completion, but it could provide a better experience when errors occur. Consider adding a retry button or providing more detailed error information.function RealtimeStatus({ runId, publicAccessToken, newOrganization, }: { runId: string; publicAccessToken: string; newOrganization: Pick<Organization, "id" | "name"> | null; }) { const t = useI18n(); const router = useRouter(); + const [hasError, setHasError] = useState(false); const { run, error } = useRealtimeRun(runId, { accessToken: publicAccessToken, onComplete: (run, error) => { if (error) { + setHasError(true); toast.error(t("common.actions.error"), { duration: 5000 }); return; } }, }); useEffect(() => { if (run?.status === "COMPLETED" && newOrganization) { router.push(`/${newOrganization.id}`); } }, [run, router, newOrganization]); return ( <div className="flex flex-col justify-center space-y-4 animate-in fade-in slide-in-from-bottom-4 duration-300"> <div className="flex flex-col gap-2 justify-center"> <LogoSpinner /> <h2 className="text-xl font-semibold text-center tracking-tight"> {t("onboarding.trigger.title")} </h2> <p className="text-center text-sm text-muted-foreground"> {t("onboarding.trigger.creating")} </p> + {hasError && ( + <Button + onClick={() => window.location.reload()} + className="mt-4" + variant="outline" + size="sm" + > + {t("common.actions.retry")} + </Button> + )} </div> </div> ); }apps/app/src/components/organization-switcher.tsx (2)
20-24: OrgItem interface could use more descriptive naming.The
OrgIteminterface represents an organization with minimal properties, but its name doesn't clearly indicate that. Consider renaming it to something more descriptive likeMinimalOrganization.-interface OrgItem { +interface MinimalOrganization { id: string; name?: string; logo_url?: string; }
85-114: Portal implementation for loading state is effective but has potential issues.The portal implementation for showing a loading overlay during organization switching is effective, but the global styles that manipulate the body and
#__nextelements could potentially conflict with other UI components or cause unexpected side effects.Consider using a more isolated approach for the overlay:
- <style jsx global>{` - body { - overflow: hidden !important; - } - - #__next > *:not(.org-switch-overlay) { - pointer-events: none !important; - filter: blur(8px) !important; - opacity: 0.6 !important; - } - `}</style> + <div className="fixed inset-0 z-[999999] pointer-events-auto"> + <div className="w-full h-full bg-background/80 backdrop-blur-sm" /> + {/* Rest of overlay content */} + </div>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/app/src/components/modals/create-org-modal.tsx(1 hunks)apps/app/src/components/organization-switcher.tsx(1 hunks)packages/ui/src/components/dialog.tsx(1 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
apps/app/src/components/modals/create-org-modal.tsx (1)
packages/ui/src/components/dialog.tsx (5) (5)
DialogContent(136-136)DialogHeader(137-137)DialogTitle(139-139)DialogDescription(140-140)DialogFooter(138-138)
🔇 Additional comments (13)
packages/ui/src/components/dialog.tsx (2)
32-34: Appropriate property addition for enhanced dialog control.The new
hideOverlayCloseproperty allows for preventing dialogs from closing when users click outside the modal area. This is a useful addition for scenarios where accidental dismissal could lead to data loss or interrupt important processes.
35-35: Implementation correctly prevents overlay dismissal.The implementation properly leverages the
onPointerDownOutsideevent handler to conditionally prevent the default closing behavior whenhideOverlayCloseis true. This provides the expected functionality while maintaining the default behavior when the property is not specified.Also applies to: 40-40
apps/app/src/components/modals/create-org-modal.tsx (5)
32-35: Clean props interface design.The props interface is well-structured and clearly defines the component's requirements. The
onOpenChangecallback allows parent components to control the modal's visibility, while theframeworksarray provides necessary data for the form.
41-47: State management approach for tracking creation progress.The component uses appropriate state variables to track the organization creation process. Using
useStateforrunIdandpublicAccessToken, anduseReffor the newly created organization is a good approach to maintain this information across renders.
74-75: Logic ensures modal stays open during execution.The
handleOpenChangefunction properly prevents the dialog from closing when the organization creation process is executing. This ensures users don't accidentally cancel an in-progress operation.
83-83: Appropriate use of hideOverlayClose property.The component correctly uses the new
hideOverlayCloseproperty when the creation process is executing. This prevents users from accidentally closing the modal by clicking outside it during critical operations.
103-111: Effective use of conditional rendering for process status.The component conditionally renders the
RealtimeStatuscomponent when a run is in progress, which provides good user feedback during the asynchronous operation.apps/app/src/components/organization-switcher.tsx (6)
4-19: Comprehensive import structure supports new UI functionality.The updated imports properly include all the necessary dependencies for the enhanced organization switcher functionality, including the new modal components, animations, and Portal functionality.
26-30: Props interface appropriately updated to include frameworks.The
OrganizationSwitcherPropsinterface has been properly updated to include theframeworksproperty that will be passed to the create organization modal.
37-42: Well-designed state management for UI interactions.The component uses appropriate state variables to track the modal's open state, the active state for the organization list, and loading state during organization changes.
72-75: Change organization handling is straightforward and effective.The organization change handler correctly sets the loading state before executing the action, providing a good user experience during the transition.
81-81: Smart organization sorting improves UX.Sorting the organizations to show the current one first, followed by others, creates a more intuitive user experience.
145-161: CreateOrgModal integration is well implemented.The integration of the
CreateOrgModalcomponent is well done, using the Dialog component to control its visibility and passing the necessary frameworks data.
| const { execute } = useAction(changeOrganizationAction, { | ||
| onSuccess: (result) => { | ||
| if (result.data?.success) { | ||
| // Add a 2 second delay before navigation | ||
| setTimeout(() => { | ||
| const currentPath = window.location.pathname; | ||
| const currentPathParts = currentPath.split('/'); | ||
| // Replace the organization ID part with the new one | ||
| if (currentPathParts.length > 1) { | ||
| currentPathParts[1] = result.data?.data?.id || ''; | ||
| router.push(currentPathParts.join('/')); | ||
| } else { | ||
| router.push(`/${result.data?.data?.id}`); | ||
| } | ||
| router.refresh(); | ||
| }, 1500); | ||
| } | ||
| }, | ||
| onError: (error) => { | ||
| console.error(error); | ||
| setIsLoading(false); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
💡 Verification agent
❓ Verification inconclusive
Delayed navigation could cause confusion without visual feedback.
The implementation adds a 1.5-second delay before navigation when switching organizations. While there is a loading state, consider adding a more explicit message or progress indicator to inform users about the navigation delay.
🏁 Script executed:
#!/bin/bash
# Description: Check if there's proper loading state UI feedback for delayed navigation
# Check for loading indicators or messages during organization switching
rg -A 5 "switching" --glob "*.tsx" --glob "*.ts"Length of output: 48
Add explicit progress indicator during delayed navigation
- The current implementation uses a 1.5‑second delay after successfully switching organizations before navigating, but there’s no clear visual feedback (e.g., a message or spinner) during this interval.
- It’s recommended to enhance the user experience by displaying a progress indicator or an explicit message during the delay, ensuring users understand that the organization switch is in progress.
- Affected File:
apps/app/src/components/organization-switcher.tsx(lines 43–65)
| <Dialog open={isOpen} onOpenChange={onOpenChange}> | ||
| <motion.div ref={ref as React.RefObject<HTMLDivElement>} layout className="w-[32px] h-[32px] relative"> | ||
| {[ | ||
| ...sortedOrganizations.filter(Boolean).map(org => ({ | ||
| org: { | ||
| id: org?.id || "", | ||
| name: org?.name || "", | ||
| } as OrgItem | ||
| })), | ||
| { org: { id: "add" } as OrgItem } | ||
| ].map(({ org }, index) => ( | ||
| <motion.div | ||
| key={org.id} | ||
| className={cn( | ||
| "w-[32px] h-[32px] left-0 overflow-hidden absolute", | ||
| isLoading && "opacity-50 pointer-events-none" | ||
| )} | ||
| style={{ zIndex: -index }} | ||
| initial={{ | ||
| scale: `${100 - index * 16}%`, | ||
| y: index * 5, | ||
| }} | ||
| {...(isActive && { | ||
| animate: { | ||
| y: -(32 + 10) * index, | ||
| scale: "100%", | ||
| }, | ||
| })} | ||
| > | ||
| {org.id === "add" ? ( | ||
| <> | ||
| <Button | ||
| className="w-[32px] h-[32px]" | ||
| size="icon" | ||
| variant="outline" | ||
| onClick={() => { | ||
| onOpenChange(true); | ||
| setActive(false); | ||
| }} | ||
| disabled={isLoading} | ||
| > | ||
| <Icons.Add /> | ||
| </Button> | ||
|
|
||
| return ( | ||
| <DropdownMenu> | ||
| <DropdownMenuTrigger asChild> | ||
| <Button variant="outline" className="flex items-center gap-2"> | ||
| <Building2 className="h-4 w-4" /> | ||
| <span className="hidden md:inline-block"> | ||
| {currentOrganization?.name} | ||
| </span> | ||
| <ChevronDown className="h-4 w-4" /> | ||
| </Button> | ||
| </DropdownMenuTrigger> | ||
| <DropdownMenuContent align="start" className="w-52"> | ||
| {organizations.map((org) => ( | ||
| <DropdownMenuItem | ||
| key={org.id} | ||
| onClick={() => handleOrganizationChange(org.id)} | ||
| className="flex items-center gap-2 cursor-pointer" | ||
| > | ||
| <span>{org.name}</span> | ||
| </DropdownMenuItem> | ||
| ))} | ||
| <DropdownMenuItem | ||
| className="flex items-center gap-2 cursor-pointer" | ||
| onClick={() => { | ||
| router.push("/setup"); | ||
| }} | ||
| > | ||
| <Plus className="h-4 w-4" /> | ||
| <span>Create new org</span> | ||
| </DropdownMenuItem> | ||
| </DropdownMenuContent> | ||
| </DropdownMenu> | ||
| ); | ||
| <CreateOrgModal onOpenChange={onOpenChange} frameworks={frameworks} /> | ||
| </> | ||
| ) : ( | ||
| <Avatar | ||
| className={cn( | ||
| "w-[32px] h-[32px] rounded-none border border-[#DCDAD2] dark:border-[#2C2C2C] cursor-pointer", | ||
| isLoading && "cursor-not-allowed" | ||
| )} | ||
| onClick={() => { | ||
| if (isLoading) return; | ||
| if (index === 0) { | ||
| setActive(true); | ||
| } else { | ||
| handleOrganizationChange(org.id); | ||
| } | ||
| }} | ||
| > | ||
| <AvatarImageNext | ||
| src={org?.logo_url || ""} | ||
| alt={org?.name ?? ""} | ||
| width={20} | ||
| height={20} | ||
| quality={100} | ||
| /> | ||
| <AvatarFallback className="rounded-none w-[32px] h-[32px]"> | ||
| <span className="text-xs"> | ||
| {org?.name?.charAt(0)?.toUpperCase()} | ||
| {org?.name?.charAt(1)?.toUpperCase()} | ||
| </span> | ||
| </AvatarFallback> | ||
| </Avatar> | ||
| )} | ||
| </motion.div> | ||
| ))} | ||
| </motion.div> | ||
| </Dialog> |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Animation approach may cause performance issues on lower-end devices.
The implementation uses complex animations with framer-motion for the organization switching UI. While visually appealing, these animations might cause performance issues on lower-end devices, especially with multiple organization avatars.
Consider adding a check to verify if the user has reduced motion preferences:
🌐 Web query:
What's the best way to detect and respect reduced motion preferences in React applications?
💡 Result:
To detect and respect reduced motion preferences in React applications, developers can implement several strategies ranging from CSS media queries to React-specific hooks and animation library integrations:
Core Detection Methods
CSS Media Queries
Use prefers-reduced-motion to conditionally apply styles:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}This globally disables animations for users with system-level reduced motion enabled [2][6].
JavaScript Detection
Use window.matchMedia to access the preference programmatically:
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
const prefersReducedMotion = motionQuery.matches;This enables dynamic animation adjustments in JavaScript logic [4][16].
React-Specific Solutions
Custom Hook Implementation
Create a reusable hook for motion preferences:
import { useEffect, useState } from 'react';
const useReducedMotion = () => {
const [prefersReduced, setPrefersReduced] = useState(true);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
setPrefersReduced(mediaQuery.matches);
const listener = (e) => setPrefersReduced(e.matches);
mediaQuery.addEventListener('change', listener);
return () => mediaQuery.removeEventListener('change', listener);
}, []);
return prefersReduced;
};Handles SSR compatibility by initializing state conservatively [5][12].
Animation Library Integration
With Framer Motion:
import { motion, useReducedMotion } from 'framer-motion';
const AnimatedComponent = () => {
const shouldReduce = useReducedMotion();
return (
<motion.div
animate={{ x: shouldReduce ? 0 : 100 }}
transition={{ duration: shouldReduce ? 0 : 1 }}
/>
);
};The built-in useReducedMotion hook automatically responds to preference changes [14].
Advanced Techniques
User-Controlled Overrides
Implement a toggle that persists in localStorage:
const MotionToggle = () => {
const [reduced, setReduced] = useLocalStorage('reducedMotion', false);
useEffect(() => {
document.documentElement.dataset.reducedMotion = reduced;
}, [reduced]);
return (
<label>
<input
type="checkbox"
checked={reduced}
onChange={(e) => setReduced(e.target.checked)}
/>
Reduce Motion
</label>
);
};Combines system preferences with user choice [3][17].
Performance Optimization
Conditionally load animation libraries:
useEffect(() => {
if (!prefersReducedMotion) {
import('gsap').then(({ gsap }) => {
// Initialize animations
});
}
}, [prefersReducedMotion]);Reduces bundle size for users who won't use animations [4].
Platform-Specific Notes
React Native
Use built-in APIs:
import { AccessibilityInfo } from 'react-native';
const [reduceMotion, setReduceMotion] = useState(false);
useEffect(() => {
AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);
}, []);Handles platform-level motion settings [7].
SSR Considerations
For Next.js/Gatsby, use safe initialization:
const [prefersReduced, setPrefersReduced] = useState(true);
useEffect(() => {
setPrefersReduced(window.matchMedia('(prefers-reduced-motion: reduce)').matches);
}, []);Avoids hydration mismatches [5].
By combining system preference detection, user-controlled toggles, and library integrations, React applications can provide accessible motion experiences while maintaining engaging interactions for users without motion sensitivities.
Citations:
- 1: https://docs.swmansion.com/react-native-reanimated/docs/guides/accessibility
- 2: https://www.smashingmagazine.com/2020/09/design-reduced-motion-sensitivities/
- 3: https://www.a11ywithlindsey.com/blog/reducing-motion-improve-accessibility/
- 4: https://www.smashingmagazine.com/2021/10/respecting-users-motion-preferences/
- 5: https://joshwcomeau.com/react/prefers-reduced-motion/
- 6: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion
- 7: https://reactnative.dev/docs/accessibilityinfo
- 8: https://kittygiraudel.com/2020/11/20/user-preferences-in-react/
- 9: https://web.dev/articles/prefers-reduced-motion
- 10: https://www.angularminds.com/blog/must-know-tips-and-tricks-to-optimize-performance-in-react-animations
- 11: https://frontendmasters.com/courses/react-accessibility/reduced-motion-exercise/
- 12: https://since1979.dev/respecting-prefers-reduced-motion-with-javascript-and-react/
- 13: Accessibility, add support for prefers-reduced-motion reactjs/react-transition-group#861
- 14: https://motion.dev/docs/react-use-reduced-motion
- 15: https://github.com/Slashgear/use-reduced-motion
- 16: https://www.letsbuildui.dev/articles/reducing-motion-in-animations/
- 17: https://kittygiraudel.com/2018/03/19/implementing-a-reduced-motion-mode/
- 18: https://github.com/infiniteluke/react-reduce-motion
Performance Concern: Implement Reduced Motion Handling
The current animation implementation using Framer Motion may lead to performance issues on lower-end devices or for users with motion sensitivities. To address this, please consider detecting and respecting reduced motion preferences. For example, you could use Framer Motion’s built-in useReducedMotion hook (or apply CSS media queries with prefers-reduced-motion) to conditionally disable or simplify animations. One potential approach:
-
Integrate Reduced Motion Detection:
- Import and use the
useReducedMotionhook from Framer Motion. - Modify animation properties conditionally to either disable or reduce animation effects when reduced motion is preferred.
- Import and use the
-
Example Code Update:
import { motion, useReducedMotion } from 'framer-motion'; const OrganizationSwitcher = (props) => { const shouldReduceMotion = useReducedMotion(); return ( <Dialog open={isOpen} onOpenChange={onOpenChange}> <motion.div ref={ref as React.RefObject<HTMLDivElement>} layout className="w-[32px] h-[32px] relative" > {[ ...sortedOrganizations.filter(Boolean).map(org => ({ org: { id: org?.id || "", name: org?.name || "", } as OrgItem })), { org: { id: "add" } as OrgItem } ].map(({ org }, index) => ( <motion.div key={org.id} className={cn( "w-[32px] h-[32px] left-0 overflow-hidden absolute", isLoading && "opacity-50 pointer-events-none" )} style={{ zIndex: -index }} initial={{ scale: shouldReduceMotion ? "100%" : `${100 - index * 16}%`, y: shouldReduceMotion ? 0 : index * 5, }} {...(!shouldReduceMotion && { animate: { y: -(32 + 10) * index, scale: "100%", }, })} transition={{ duration: shouldReduceMotion ? 0 : 0.5 }} > {org.id === "add" ? ( <> <Button className="w-[32px] h-[32px]" size="icon" variant="outline" onClick={() => { onOpenChange(true); setActive(false); }} disabled={isLoading} > <Icons.Add /> </Button> <CreateOrgModal onOpenChange={onOpenChange} frameworks={frameworks} /> </> ) : ( <Avatar className={cn( "w-[32px] h-[32px] rounded-none border border-[#DCDAD2] dark:border-[#2C2C2C] cursor-pointer", isLoading && "cursor-not-allowed" )} onClick={() => { if (isLoading) return; if (index === 0) { setActive(true); } else { handleOrganizationChange(org.id); } }} > <AvatarImageNext src={org?.logo_url || ""} alt={org?.name ?? ""} width={20} height={20} quality={100} /> <AvatarFallback className="rounded-none w-[32px] h-[32px]"> <span className="text-xs"> {org?.name?.charAt(0)?.toUpperCase()} {org?.name?.charAt(1)?.toUpperCase()} </span> </AvatarFallback> </Avatar> )} </motion.div> ))} </motion.div> </Dialog> ); };
This update will help ensure that users who have enabled reduced motion preferences experience a smoother and more accessible UI. Please verify that this approach works as expected across target devices.
Summary by CodeRabbit
New Features
Bug Fixes
Refactor
Style
Chores