Fix team page

This commit is contained in:
Urtzi Alfaro
2025-09-12 23:58:26 +02:00
parent 4c21a5e1b2
commit 96da9ca077
4 changed files with 522 additions and 204 deletions

View File

@@ -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 */}