fix(frontend): resolve lint blockers for gitea frontend-ci
This commit is contained in:
@@ -15,7 +15,7 @@ import { test, expect } from '@playwright/test';
|
||||
/**
|
||||
* Robust club selection helper with fallback locators
|
||||
*/
|
||||
async function selectClubIfPresent(page: any) {
|
||||
async function selectClubIfPresent(page: import('@playwright/test').Page) {
|
||||
const isOnSelectClub = page.url().includes('/select-club');
|
||||
|
||||
if (!isOnSelectClub) {
|
||||
@@ -182,7 +182,7 @@ test.describe('Authentication Flow', () => {
|
||||
});
|
||||
});
|
||||
|
||||
async function authenticateUser(page: any, email: string, password: string) {
|
||||
async function authenticateUser(page: import('@playwright/test').Page, email: string, password: string) {
|
||||
await page.goto('/login');
|
||||
await page.click('button:has-text("Sign in with Keycloak")');
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { test, expect } from '@playwright/test';
|
||||
* - Visual capacity indicators (progress bar, spot counts)
|
||||
*/
|
||||
|
||||
async function selectClubIfPresent(page: any) {
|
||||
async function selectClubIfPresent(page: import('@playwright/test').Page) {
|
||||
const isOnSelectClub = page.url().includes('/select-club');
|
||||
|
||||
if (!isOnSelectClub) {
|
||||
@@ -51,7 +51,7 @@ async function selectClubIfPresent(page: any) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loginAs(page: any, email: string, password: string) {
|
||||
async function loginAs(page: import('@playwright/test').Page, email: string, password: string) {
|
||||
await page.goto('/login');
|
||||
await page.click('button:has-text("Sign in with Keycloak")');
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { AuthGuard } from '@/components/auth-guard';
|
||||
import { ClubSwitcher } from '@/components/club-switcher';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LogOut } from 'lucide-react';
|
||||
import { SignOutButton } from '@/components/sign-out-button';
|
||||
|
||||
export default function ProtectedLayout({
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { use } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTask, useUpdateTask } from '@/hooks/useTasks';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -25,7 +24,6 @@ const statusColors: Record<string, string> = {
|
||||
|
||||
export default function TaskDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const resolvedParams = use(params);
|
||||
const router = useRouter();
|
||||
const { data: task, isLoading, error } = useTask(resolvedParams.id);
|
||||
const { mutate: updateTask, isPending } = useUpdateTask();
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
async jwt({ token, account }) {
|
||||
if (account) {
|
||||
// Add clubs claim from Keycloak access token
|
||||
token.clubs = (account as any).clubs || {}
|
||||
token.clubs = (account as Record<string, unknown>).clubs as Record<string, string> || {}
|
||||
token.accessToken = account.access_token
|
||||
}
|
||||
return token
|
||||
|
||||
@@ -22,28 +22,28 @@ describe('AuthGuard', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(useRouter as any).mockReturnValue({ push: mockPush } as any);
|
||||
(useRouter as ReturnType<typeof vi.fn>).mockReturnValue({ push: mockPush });
|
||||
});
|
||||
|
||||
it('renders loading state when session is loading', () => {
|
||||
(useSession as any).mockReturnValue({ data: null, status: 'loading' } as any);
|
||||
(useTenant as any).mockReturnValue({ activeClubId: null, clubs: [], setActiveClub: vi.fn(), userRole: null });
|
||||
(useSession as ReturnType<typeof vi.fn>).mockReturnValue({ data: null, status: 'loading' });
|
||||
(useTenant as ReturnType<typeof vi.fn>).mockReturnValue({ activeClubId: null, clubs: [], setActiveClub: vi.fn(), userRole: null });
|
||||
|
||||
render(<AuthGuard><div>Protected</div></AuthGuard>);
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects to /login when unauthenticated', () => {
|
||||
(useSession as any).mockReturnValue({ data: null, status: 'unauthenticated' } as any);
|
||||
(useTenant as any).mockReturnValue({ activeClubId: null, clubs: [], setActiveClub: vi.fn(), userRole: null });
|
||||
(useSession as ReturnType<typeof vi.fn>).mockReturnValue({ data: null, status: 'unauthenticated' });
|
||||
(useTenant as ReturnType<typeof vi.fn>).mockReturnValue({ activeClubId: null, clubs: [], setActiveClub: vi.fn(), userRole: null });
|
||||
|
||||
render(<AuthGuard><div>Protected</div></AuthGuard>);
|
||||
expect(mockPush).toHaveBeenCalledWith('/login');
|
||||
});
|
||||
|
||||
it('shows Contact admin when 0 clubs', () => {
|
||||
(useSession as any).mockReturnValue({ data: { user: {} }, status: 'authenticated' } as any);
|
||||
(useTenant as any).mockReturnValue({ activeClubId: null, clubs: [], setActiveClub: vi.fn(), userRole: null });
|
||||
(useSession as ReturnType<typeof vi.fn>).mockReturnValue({ data: { user: {} }, status: 'authenticated' });
|
||||
(useTenant as ReturnType<typeof vi.fn>).mockReturnValue({ activeClubId: null, clubs: [], setActiveClub: vi.fn(), userRole: null });
|
||||
|
||||
render(<AuthGuard><div>Protected</div></AuthGuard>);
|
||||
expect(screen.getByText('Contact admin to get access to a club')).toBeInTheDocument();
|
||||
@@ -51,39 +51,39 @@ describe('AuthGuard', () => {
|
||||
|
||||
it('auto-selects when 1 club and no active club', () => {
|
||||
const mockSetActiveClub = vi.fn();
|
||||
(useSession as any).mockReturnValue({ data: { user: {} }, status: 'authenticated' } as any);
|
||||
(useTenant as any).mockReturnValue({
|
||||
(useSession as ReturnType<typeof vi.fn>).mockReturnValue({ data: { user: {} }, status: 'authenticated' });
|
||||
(useTenant as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
activeClubId: null,
|
||||
clubs: [{ id: 'club-1', name: 'Club 1' }],
|
||||
setActiveClub: mockSetActiveClub,
|
||||
userRole: null
|
||||
} as any);
|
||||
});
|
||||
|
||||
render(<AuthGuard><div>Protected</div></AuthGuard>);
|
||||
expect(mockSetActiveClub).toHaveBeenCalledWith('club-1');
|
||||
});
|
||||
|
||||
it('redirects to /select-club when multiple clubs and no active club', () => {
|
||||
(useSession as any).mockReturnValue({ data: { user: {} }, status: 'authenticated' } as any);
|
||||
(useTenant as any).mockReturnValue({
|
||||
(useSession as ReturnType<typeof vi.fn>).mockReturnValue({ data: { user: {} }, status: 'authenticated' });
|
||||
(useTenant as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
activeClubId: null,
|
||||
clubs: [{ id: 'club-1', name: 'Club 1' }, { id: 'club-2', name: 'Club 2' }],
|
||||
setActiveClub: vi.fn(),
|
||||
userRole: null
|
||||
} as any);
|
||||
});
|
||||
|
||||
render(<AuthGuard><div>Protected</div></AuthGuard>);
|
||||
expect(mockPush).toHaveBeenCalledWith('/select-club');
|
||||
});
|
||||
|
||||
it('renders children when authenticated and active club is set', () => {
|
||||
(useSession as any).mockReturnValue({ data: { user: {} }, status: 'authenticated' } as any);
|
||||
(useTenant as any).mockReturnValue({
|
||||
(useSession as ReturnType<typeof vi.fn>).mockReturnValue({ data: { user: {} }, status: 'authenticated' });
|
||||
(useTenant as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
activeClubId: 'club-1',
|
||||
clubs: [{ id: 'club-1', name: 'Club 1' }],
|
||||
setActiveClub: vi.fn(),
|
||||
userRole: 'admin'
|
||||
} as any);
|
||||
});
|
||||
|
||||
render(<AuthGuard><div>Protected Content</div></AuthGuard>);
|
||||
expect(screen.getByText('Protected Content')).toBeInTheDocument();
|
||||
|
||||
@@ -9,7 +9,7 @@ vi.mock('../../contexts/tenant-context', () => ({
|
||||
|
||||
vi.mock('../ui/dropdown-menu', () => ({
|
||||
DropdownMenu: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DropdownMenuTrigger: ({ children, asChild }: { children: React.ReactNode, asChild?: boolean }) => <div data-testid="trigger">{children}</div>,
|
||||
DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => <div data-testid="trigger">{children}</div>,
|
||||
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => <div data-testid="content">{children}</div>,
|
||||
DropdownMenuItem: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => <div onClick={onClick} data-testid="menu-item">{children}</div>,
|
||||
DropdownMenuLabel: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
@@ -22,19 +22,19 @@ describe('ClubSwitcher', () => {
|
||||
});
|
||||
|
||||
it('renders loading state when clubs is empty', () => {
|
||||
(useTenant as any).mockReturnValue({
|
||||
(useTenant as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
activeClubId: null,
|
||||
clubs: [],
|
||||
setActiveClub: vi.fn(),
|
||||
userRole: null
|
||||
} as any);
|
||||
});
|
||||
|
||||
render(<ClubSwitcher />);
|
||||
expect(screen.getByRole('button')).toHaveTextContent('Select Club');
|
||||
});
|
||||
|
||||
it('renders current club name and sport type badge', () => {
|
||||
(useTenant as any).mockReturnValue({
|
||||
(useTenant as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
activeClubId: 'club-1',
|
||||
clubs: [
|
||||
{ id: 'club-1', name: 'Tennis Club', sportType: 'Tennis' },
|
||||
@@ -42,7 +42,7 @@ describe('ClubSwitcher', () => {
|
||||
],
|
||||
setActiveClub: vi.fn(),
|
||||
userRole: 'admin'
|
||||
} as any);
|
||||
});
|
||||
|
||||
render(<ClubSwitcher />);
|
||||
expect(screen.getAllByText('Tennis Club')[0]).toBeInTheDocument();
|
||||
@@ -50,7 +50,7 @@ describe('ClubSwitcher', () => {
|
||||
|
||||
it('calls setActiveClub when club is selected', () => {
|
||||
const mockSetActiveClub = vi.fn();
|
||||
(useTenant as any).mockReturnValue({
|
||||
(useTenant as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
activeClubId: 'club-1',
|
||||
clubs: [
|
||||
{ id: 'club-1', name: 'Tennis Club', sportType: 'Tennis' },
|
||||
@@ -58,7 +58,7 @@ describe('ClubSwitcher', () => {
|
||||
],
|
||||
setActiveClub: mockSetActiveClub,
|
||||
userRole: 'admin'
|
||||
} as any);
|
||||
});
|
||||
|
||||
render(<ClubSwitcher />);
|
||||
|
||||
|
||||
@@ -38,12 +38,12 @@ describe('ShiftDetailPage', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(useSignUpShift as any).mockReturnValue({ mutateAsync: mockSignUp, isPending: false });
|
||||
(useCancelSignUp as any).mockReturnValue({ mutateAsync: mockCancel, isPending: false });
|
||||
(useSignUpShift as ReturnType<typeof vi.fn>).mockReturnValue({ mutateAsync: mockSignUp, isPending: false });
|
||||
(useCancelSignUp as ReturnType<typeof vi.fn>).mockReturnValue({ mutateAsync: mockCancel, isPending: false });
|
||||
});
|
||||
|
||||
it('shows "Sign Up" button if capacity available', async () => {
|
||||
(useShift as any).mockReturnValue({
|
||||
(useShift as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
data: {
|
||||
id: '1',
|
||||
title: 'Detail Shift',
|
||||
@@ -69,7 +69,7 @@ describe('ShiftDetailPage', () => {
|
||||
});
|
||||
|
||||
it('shows "Cancel Sign-up" button if user is signed up', async () => {
|
||||
(useShift as any).mockReturnValue({
|
||||
(useShift as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
data: {
|
||||
id: '1',
|
||||
title: 'Detail Shift',
|
||||
@@ -95,7 +95,7 @@ describe('ShiftDetailPage', () => {
|
||||
});
|
||||
|
||||
it('calls sign up mutation on click', async () => {
|
||||
(useShift as any).mockReturnValue({
|
||||
(useShift as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
data: {
|
||||
id: '1',
|
||||
title: 'Detail Shift',
|
||||
|
||||
@@ -20,18 +20,18 @@ describe('TaskDetailPage', () => {
|
||||
const mockMutate = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
(useUpdateTask as any).mockReturnValue({
|
||||
(useUpdateTask as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
mutate: mockMutate,
|
||||
isPending: false,
|
||||
} as any);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows valid transitions for Open status', async () => {
|
||||
(useTask as any).mockReturnValue({
|
||||
(useTask as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
data: { id: '1', title: 'Task 1', status: 'Open', description: 'Desc', createdAt: '2024-01-01', updatedAt: '2024-01-01' },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as any);
|
||||
});
|
||||
|
||||
const params = Promise.resolve({ id: '1' });
|
||||
await act(async () => {
|
||||
@@ -44,11 +44,11 @@ describe('TaskDetailPage', () => {
|
||||
});
|
||||
|
||||
it('shows valid transitions for InProgress status', async () => {
|
||||
(useTask as any).mockReturnValue({
|
||||
(useTask as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
data: { id: '1', title: 'Task 1', status: 'InProgress', description: 'Desc', createdAt: '2024-01-01', updatedAt: '2024-01-01' },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as any);
|
||||
});
|
||||
|
||||
const params = Promise.resolve({ id: '1' });
|
||||
await act(async () => {
|
||||
@@ -60,11 +60,11 @@ describe('TaskDetailPage', () => {
|
||||
});
|
||||
|
||||
it('shows valid transitions for Review status (including back transition)', async () => {
|
||||
(useTask as any).mockReturnValue({
|
||||
(useTask as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
data: { id: '1', title: 'Task 1', status: 'Review', description: 'Desc', createdAt: '2024-01-01', updatedAt: '2024-01-01' },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as any);
|
||||
});
|
||||
|
||||
const params = Promise.resolve({ id: '1' });
|
||||
await act(async () => {
|
||||
|
||||
@@ -24,7 +24,7 @@ vi.mock('@/hooks/useTasks', () => ({
|
||||
|
||||
describe('TaskListPage', () => {
|
||||
beforeEach(() => {
|
||||
(useTasks as any).mockReturnValue({
|
||||
(useTasks as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
data: {
|
||||
items: [
|
||||
{ id: '1', title: 'Test Task 1', status: 'Open', assigneeId: null, createdAt: '2024-01-01' },
|
||||
@@ -37,7 +37,7 @@ describe('TaskListPage', () => {
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as any);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders task list with 3 data rows', () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ReactNode, useEffect } from 'react';
|
||||
import { useTenant } from '../contexts/tenant-context';
|
||||
|
||||
export function AuthGuard({ children }: { children: ReactNode }) {
|
||||
const { data: session, status } = useSession();
|
||||
const { status } = useSession();
|
||||
const { activeClubId, clubs, setActiveClub, clubsLoading } = useTenant();
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||
import { createContext, useContext, useEffect, useState, useMemo, ReactNode } from 'react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
@@ -22,10 +22,31 @@ type TenantContextType = {
|
||||
|
||||
const TenantContext = createContext<TenantContextType | undefined>(undefined);
|
||||
|
||||
function getInitialClubId(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem('activeClubId');
|
||||
}
|
||||
|
||||
function determineActiveClub(clubs: Club[], currentActiveId: string | null): string | null {
|
||||
if (!clubs.length) return null;
|
||||
|
||||
const stored = getInitialClubId();
|
||||
if (stored && clubs.find(c => c.id === stored)) {
|
||||
return stored;
|
||||
}
|
||||
|
||||
if (currentActiveId && clubs.find(c => c.id === currentActiveId)) {
|
||||
return currentActiveId;
|
||||
}
|
||||
|
||||
return clubs[0].id;
|
||||
}
|
||||
|
||||
export function TenantProvider({ children }: { children: ReactNode }) {
|
||||
const { data: session, status } = useSession();
|
||||
const [activeClubId, setActiveClubId] = useState<string | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [activeClubId, setActiveClubId] = useState<string | null>(getInitialClubId);
|
||||
|
||||
const { data: clubs = [], isLoading: clubsLoading, error: clubsError } = useQuery<Club[]>({
|
||||
queryKey: ['my-clubs', session?.accessToken],
|
||||
@@ -43,25 +64,19 @@ export function TenantProvider({ children }: { children: ReactNode }) {
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * Math.pow(2, attemptIndex), 10000),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'authenticated' && clubs.length > 0) {
|
||||
const stored = localStorage.getItem('activeClubId');
|
||||
if (stored && clubs.find(c => c.id === stored)) {
|
||||
setActiveClubId(stored);
|
||||
} else if (!activeClubId) {
|
||||
setActiveClubId(clubs[0].id);
|
||||
}
|
||||
}
|
||||
const computedActiveClubId = useMemo(() => {
|
||||
if (status !== 'authenticated' || !clubs.length) return activeClubId;
|
||||
return determineActiveClub(clubs, activeClubId);
|
||||
}, [status, clubs, activeClubId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeClubId) {
|
||||
const selectedClub = clubs.find(c => c.id === activeClubId);
|
||||
if (computedActiveClubId) {
|
||||
const selectedClub = clubs.find(c => c.id === computedActiveClubId);
|
||||
if (selectedClub) {
|
||||
document.cookie = `X-Tenant-Id=${selectedClub.tenantId}; path=/; max-age=86400`;
|
||||
}
|
||||
}
|
||||
}, [activeClubId, clubs]);
|
||||
}, [computedActiveClubId, clubs]);
|
||||
|
||||
const handleSetActiveClub = (clubId: string) => {
|
||||
setActiveClubId(clubId);
|
||||
@@ -73,10 +88,10 @@ export function TenantProvider({ children }: { children: ReactNode }) {
|
||||
queryClient.invalidateQueries();
|
||||
};
|
||||
|
||||
const userRole = activeClubId && session?.user?.clubs ? session.user.clubs[activeClubId] || null : null;
|
||||
const userRole = computedActiveClubId && session?.user?.clubs ? session.user.clubs[computedActiveClubId] || null : null;
|
||||
|
||||
return (
|
||||
<TenantContext.Provider value={{ activeClubId, setActiveClub: handleSetActiveClub, userRole, clubs, clubsLoading, clubsError: clubsError || null }}>
|
||||
<TenantContext.Provider value={{ activeClubId: computedActiveClubId, setActiveClub: handleSetActiveClub, userRole, clubs, clubsLoading, clubsError: clubsError || null }}>
|
||||
{children}
|
||||
</TenantContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useActiveClub } from '../useActiveClub';
|
||||
import type { Session } from 'next-auth';
|
||||
|
||||
const mockUseSession = vi.fn();
|
||||
|
||||
@@ -33,15 +32,15 @@ describe('useActiveClub', () => {
|
||||
status: 'authenticated',
|
||||
});
|
||||
|
||||
(localStorage.getItem as any).mockImplementation((key: string) => {
|
||||
(localStorage.getItem as ReturnType<typeof vi.fn>).mockImplementation((key: string) => {
|
||||
return localStorageData[key] || null;
|
||||
});
|
||||
|
||||
(localStorage.setItem as any).mockImplementation((key: string, value: string) => {
|
||||
(localStorage.setItem as ReturnType<typeof vi.fn>).mockImplementation((key: string, value: string) => {
|
||||
localStorageData[key] = value;
|
||||
});
|
||||
|
||||
(localStorage.clear as any).mockImplementation(() => {
|
||||
(localStorage.clear as ReturnType<typeof vi.fn>).mockImplementation(() => {
|
||||
localStorageData = {};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
|
||||
const ACTIVE_CLUB_KEY = 'activeClubId';
|
||||
|
||||
function getStoredClubId(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem(ACTIVE_CLUB_KEY);
|
||||
}
|
||||
|
||||
function determineActiveId(clubs: Record<string, string> | undefined, currentId: string | null): string | null {
|
||||
if (!clubs || Object.keys(clubs).length === 0) return null;
|
||||
|
||||
const stored = getStoredClubId();
|
||||
if (stored && clubs[stored]) return stored;
|
||||
|
||||
if (currentId && clubs[currentId]) return currentId;
|
||||
|
||||
return Object.keys(clubs)[0];
|
||||
}
|
||||
|
||||
export interface ActiveClubData {
|
||||
activeClubId: string | null;
|
||||
role: string | null;
|
||||
@@ -14,23 +30,13 @@ export interface ActiveClubData {
|
||||
|
||||
export function useActiveClub(): ActiveClubData {
|
||||
const { data: session, status } = useSession();
|
||||
const [activeClubId, setActiveClubIdState] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === 'authenticated' && session?.user?.clubs) {
|
||||
const clubs = session.user.clubs;
|
||||
const storedClubId = localStorage.getItem(ACTIVE_CLUB_KEY);
|
||||
|
||||
if (storedClubId && clubs[storedClubId]) {
|
||||
setActiveClubIdState(storedClubId);
|
||||
} else {
|
||||
const firstClubId = Object.keys(clubs)[0];
|
||||
if (firstClubId) {
|
||||
setActiveClubIdState(firstClubId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [session, status]);
|
||||
|
||||
const [activeClubId, setActiveClubIdState] = useState<string | null>(getStoredClubId);
|
||||
|
||||
const computedActiveId = useMemo(() => {
|
||||
if (status !== 'authenticated' || !session?.user?.clubs) return activeClubId;
|
||||
return determineActiveId(session.user.clubs, activeClubId);
|
||||
}, [session, status, activeClubId]);
|
||||
|
||||
const setActiveClub = (clubId: string) => {
|
||||
if (session?.user?.clubs && session.user.clubs[clubId]) {
|
||||
@@ -40,10 +46,10 @@ export function useActiveClub(): ActiveClubData {
|
||||
};
|
||||
|
||||
const clubs = session?.user?.clubs || null;
|
||||
const role = activeClubId && clubs ? clubs[activeClubId] : null;
|
||||
const role = computedActiveId && clubs ? clubs[computedActiveId] : null;
|
||||
|
||||
return {
|
||||
activeClubId,
|
||||
activeClubId: computedActiveId,
|
||||
role,
|
||||
clubs,
|
||||
setActiveClub,
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('apiClient', () => {
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
(global.fetch as any).mockResolvedValue({
|
||||
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ data: 'test' }),
|
||||
@@ -145,7 +145,7 @@ describe('apiClient', () => {
|
||||
|
||||
await apiClient('/api/test');
|
||||
|
||||
const callHeaders = (global.fetch as any).mock.calls[0][1].headers;
|
||||
const callHeaders = (global.fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].headers;
|
||||
expect(callHeaders.Authorization).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -158,7 +158,7 @@ describe('apiClient', () => {
|
||||
|
||||
await apiClient('/api/test');
|
||||
|
||||
const callHeaders = (global.fetch as any).mock.calls[0][1].headers;
|
||||
const callHeaders = (global.fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].headers;
|
||||
expect(callHeaders['X-Tenant-Id']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,13 +6,13 @@ const localStorageMock = {
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
length: 0,
|
||||
key: vi.fn(),
|
||||
};
|
||||
|
||||
// Ensure localStorage is available on both global and globalThis
|
||||
global.localStorage = localStorageMock as any;
|
||||
globalThis.localStorage = localStorageMock as any;
|
||||
global.localStorage = localStorageMock as unknown as Storage;
|
||||
globalThis.localStorage = localStorageMock as unknown as Storage;
|
||||
|
||||
// Ensure document is available if jsdom hasn't set it up yet
|
||||
if (typeof document === 'undefined') {
|
||||
Object.defineProperty(globalThis, 'document', {
|
||||
value: {
|
||||
|
||||
Reference in New Issue
Block a user