Fix team page
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTenant } from '../../stores/tenant.store';
|
||||
import { useToast } from '../../hooks/ui/useToast';
|
||||
import { ChevronDown, Building2, Check, AlertCircle } from 'lucide-react';
|
||||
@@ -13,6 +14,13 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
||||
showLabel = true,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [dropdownPosition, setDropdownPosition] = useState<{
|
||||
top: number;
|
||||
left: number;
|
||||
right?: number;
|
||||
width: number;
|
||||
isMobile: boolean;
|
||||
}>({ top: 0, left: 0, width: 288, isMobile: false });
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
@@ -37,25 +45,103 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
||||
|
||||
// Handle click outside to close dropdown
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
||||
const target = event.target as Node;
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node) &&
|
||||
!buttonRef.current?.contains(event.target as Node)
|
||||
!dropdownRef.current.contains(target) &&
|
||||
!buttonRef.current?.contains(target)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener('touchstart', handleClickOutside);
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.removeEventListener('touchstart', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
// Recalculate position on window resize
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (isOpen) {
|
||||
calculateDropdownPosition();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [isOpen]);
|
||||
|
||||
// Calculate dropdown position
|
||||
const calculateDropdownPosition = () => {
|
||||
if (!buttonRef.current) return;
|
||||
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const isMobile = viewportWidth < 768; // md breakpoint
|
||||
|
||||
if (isMobile) {
|
||||
// On mobile, use full width with margins and position from top
|
||||
// Check if dropdown would go off bottom of screen
|
||||
const dropdownHeight = Math.min(400, viewportHeight * 0.7); // Max 70vh
|
||||
const spaceBelow = viewportHeight - buttonRect.bottom - 8;
|
||||
const shouldPositionAbove = spaceBelow < dropdownHeight && buttonRect.top > dropdownHeight;
|
||||
|
||||
setDropdownPosition({
|
||||
top: shouldPositionAbove ? buttonRect.top - dropdownHeight - 8 : buttonRect.bottom + 8,
|
||||
left: 16, // 1rem margin from screen edge
|
||||
right: 16, // For full width calculation
|
||||
width: viewportWidth - 32, // Full width minus margins
|
||||
isMobile: true,
|
||||
});
|
||||
} else {
|
||||
// Desktop positioning - align right edge of dropdown with right edge of button
|
||||
const dropdownWidth = 320; // w-80 (20rem * 16px) - slightly wider for desktop
|
||||
let left = buttonRect.right - dropdownWidth;
|
||||
|
||||
// Ensure dropdown doesn't go off the left edge of the screen
|
||||
if (left < 16) {
|
||||
left = 16;
|
||||
}
|
||||
|
||||
// Ensure dropdown doesn't go off the right edge
|
||||
if (left + dropdownWidth > viewportWidth - 16) {
|
||||
left = viewportWidth - dropdownWidth - 16;
|
||||
}
|
||||
|
||||
setDropdownPosition({
|
||||
top: buttonRect.bottom + 8,
|
||||
left: left,
|
||||
width: dropdownWidth,
|
||||
isMobile: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle dropdown open/close
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) {
|
||||
calculateDropdownPosition();
|
||||
}
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
// Handle tenant switch
|
||||
const handleTenantSwitch = async (tenantId: string) => {
|
||||
if (tenantId === currentTenant?.id) {
|
||||
@@ -104,7 +190,7 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
||||
{/* Trigger Button */}
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
onClick={toggleDropdown}
|
||||
disabled={isLoading}
|
||||
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium text-text-primary bg-bg-secondary hover:bg-bg-tertiary border border-border-secondary rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-opacity-20 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
aria-expanded={isOpen}
|
||||
@@ -124,18 +210,28 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isOpen && (
|
||||
{/* Dropdown Menu - Rendered in portal to avoid stacking context issues */}
|
||||
{isOpen && createPortal(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="absolute right-0 mt-2 w-72 bg-bg-primary border border-border-secondary rounded-lg shadow-lg z-50"
|
||||
className={`fixed bg-bg-primary border border-border-secondary rounded-lg shadow-lg z-[9999] ${
|
||||
dropdownPosition.isMobile ? 'mx-4' : ''
|
||||
}`}
|
||||
style={{
|
||||
top: `${dropdownPosition.top}px`,
|
||||
left: `${dropdownPosition.left}px`,
|
||||
width: `${dropdownPosition.width}px`,
|
||||
maxHeight: dropdownPosition.isMobile ? '70vh' : '80vh',
|
||||
}}
|
||||
role="listbox"
|
||||
aria-label="Available tenants"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-3 py-2 border-b border-border-primary">
|
||||
<h3 className="text-sm font-semibold text-text-primary">Switch Organization</h3>
|
||||
<p className="text-xs text-text-secondary">
|
||||
<div className={`border-b border-border-primary ${dropdownPosition.isMobile ? 'px-4 py-3' : 'px-3 py-2'}`}>
|
||||
<h3 className={`font-semibold text-text-primary ${dropdownPosition.isMobile ? 'text-base' : 'text-sm'}`}>
|
||||
Switch Organization
|
||||
</h3>
|
||||
<p className={`text-text-secondary ${dropdownPosition.isMobile ? 'text-sm mt-1' : 'text-xs'}`}>
|
||||
Select the organization you want to work with
|
||||
</p>
|
||||
</div>
|
||||
@@ -157,13 +253,15 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
||||
)}
|
||||
|
||||
{/* Tenant List */}
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
<div className={`overflow-y-auto ${dropdownPosition.isMobile ? 'max-h-[60vh]' : 'max-h-80'}`}>
|
||||
{availableTenants.map((tenant) => (
|
||||
<button
|
||||
key={tenant.id}
|
||||
onClick={() => handleTenantSwitch(tenant.id)}
|
||||
disabled={isLoading}
|
||||
className="w-full px-3 py-3 text-left hover:bg-bg-secondary focus:bg-bg-secondary focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className={`w-full text-left hover:bg-bg-secondary focus:bg-bg-secondary focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${
|
||||
dropdownPosition.isMobile ? 'px-4 py-4 active:bg-bg-tertiary' : 'px-3 py-3'
|
||||
}`}
|
||||
role="option"
|
||||
aria-selected={tenant.id === currentTenant?.id}
|
||||
>
|
||||
@@ -193,15 +291,20 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-3 py-2 border-t border-border-primary bg-bg-secondary rounded-b-lg">
|
||||
<p className="text-xs text-text-secondary">
|
||||
<div className={`border-t border-border-primary bg-bg-secondary rounded-b-lg ${
|
||||
dropdownPosition.isMobile ? 'px-4 py-3' : 'px-3 py-2'
|
||||
}`}>
|
||||
<p className={`text-text-secondary ${dropdownPosition.isMobile ? 'text-sm' : 'text-xs'}`}>
|
||||
Need to add a new organization?{' '}
|
||||
<button className="text-color-primary hover:text-color-primary-dark underline">
|
||||
<button className={`text-color-primary hover:text-color-primary-dark underline ${
|
||||
dropdownPosition.isMobile ? 'active:text-color-primary-dark' : ''
|
||||
}`}>
|
||||
Contact Support
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{/* Loading Overlay */}
|
||||
|
||||
Reference in New Issue
Block a user