feat: add complex user invitation and management features

- Implemented complex member and invitation schemas using Zod for validation.
- Created new API endpoints for inviting, accepting, revoking, and canceling complex user invitations.
- Developed handlers for managing complex users, including listing, inviting, revoking, and canceling invitations.
- Added frontend components for displaying and managing complex users and invitations.
- Introduced session storage management for pending invitations.
- Integrated Gravatar for user avatars based on email.
- Created database migration for complex invitations table.
This commit is contained in:
Jose Selesan
2026-04-22 15:30:04 -03:00
parent 5f2a7fd494
commit 6ad4fc9ee9
37 changed files with 1935 additions and 78 deletions

View File

@@ -1,4 +1,3 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
@@ -8,25 +7,49 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { UserAvatar } from '@/components/user-avatar';
import { apiClient } from '@/lib/api-client';
import { useAuth } from '@/lib/auth';
import { acceptStoredPendingInvite } from '@/lib/invitations';
import { useCurrentComplexStore } from '@/lib/stores/current-complex-store';
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Link, Outlet, useNavigate } from '@tanstack/react-router';
import { LogOut, Menu, UserRound } from 'lucide-react';
import { useMemo } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { ThemeSwitcher } from './theme-switcher';
export function RootLayout() {
const navigate = useNavigate();
const { isAuthenticated, displayName, initials, avatarUrl, signOut } = useAuth();
const queryClient = useQueryClient();
const { user, isAuthenticated, displayName, avatarUrl, signOut } = useAuth();
const { currentComplexSlug, setCurrentComplex } = useCurrentComplexStore();
const processedInviteForUser = useRef<string | null>(null);
const myComplexesQuery = useQuery({
queryKey: ['my-complexes'],
enabled: isAuthenticated,
queryFn: () => apiClient.complexes.listMine(),
});
useEffect(() => {
if (!isAuthenticated) {
processedInviteForUser.current = null;
return;
}
const userKey = user?.email ?? displayName ?? 'authenticated-user';
if (processedInviteForUser.current === userKey) {
return;
}
processedInviteForUser.current = userKey;
void (async () => {
await acceptStoredPendingInvite(apiClient.complexes.acceptPendingInvitations);
await queryClient.invalidateQueries({ queryKey: ['my-complexes'] });
})();
}, [displayName, isAuthenticated, queryClient, user?.email]);
const complexSlug = currentComplexSlug || myComplexesQuery.data?.[0]?.complexSlug;
const currentComplexName = useMemo(() => {
@@ -85,10 +108,12 @@ export function RootLayout() {
type="button"
className="hidden items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-muted md:inline-flex"
>
<Avatar size="sm">
<AvatarImage src={avatarUrl ?? undefined} alt={displayName} />
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<UserAvatar
src={avatarUrl}
alt={displayName}
fallbackText={displayName}
size="sm"
/>
<span className="max-w-32 truncate text-sm text-foreground">{displayName}</span>
</button>
</DropdownMenuTrigger>