forked from cardosofelipe/fast-next-template
Refactor ESLint configuration and update test rules for clarity and consistency
- Consolidated and modularized `eslint.config.mjs` with defined rules for source, test, E2E, and scripts. - Improved test and E2E rules with relaxed settings for flexibility and enhanced mocking. - Standardized variable naming and removed redundant imports in unit and E2E tests. - Updated error handling and comments to align with modern TypeScript best practices (e.g., `@ts-expect-error`).
This commit is contained in:
@@ -7,7 +7,6 @@ import { test, expect } from '@playwright/test';
|
|||||||
import {
|
import {
|
||||||
setupAuthenticatedMocks,
|
setupAuthenticatedMocks,
|
||||||
setupSuperuserMocks,
|
setupSuperuserMocks,
|
||||||
loginViaUI,
|
|
||||||
} from './helpers/auth';
|
} from './helpers/auth';
|
||||||
|
|
||||||
test.describe('Admin Access Control', () => {
|
test.describe('Admin Access Control', () => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { setupSuperuserMocks, loginViaUI } from './helpers/auth';
|
import { setupSuperuserMocks } from './helpers/auth';
|
||||||
|
|
||||||
test.describe('Admin Dashboard - Page Load', () => {
|
test.describe('Admin Dashboard - Page Load', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { setupSuperuserMocks, loginViaUI } from './helpers/auth';
|
import { setupSuperuserMocks } from './helpers/auth';
|
||||||
|
|
||||||
test.describe('Admin Organization Members - Navigation from Organizations List', () => {
|
test.describe('Admin Organization Members - Navigation from Organizations List', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { setupSuperuserMocks, loginViaUI } from './helpers/auth';
|
import { setupSuperuserMocks } from './helpers/auth';
|
||||||
|
|
||||||
test.describe('Admin Organization Management - Page Load', () => {
|
test.describe('Admin Organization Management - Page Load', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { setupSuperuserMocks, loginViaUI } from './helpers/auth';
|
import { setupSuperuserMocks } from './helpers/auth';
|
||||||
|
|
||||||
test.describe('Admin User Management - Page Load', () => {
|
test.describe('Admin User Management - Page Load', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
* - Savings: ~690s (~11 minutes) per test run
|
* - Savings: ~690s (~11 minutes) per test run
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { test as setup, expect } from '@playwright/test';
|
import { test as setup } from '@playwright/test';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { setupAuthenticatedMocks, setupSuperuserMocks, loginViaUI } from './helpers/auth';
|
import { setupAuthenticatedMocks, setupSuperuserMocks } from './helpers/auth';
|
||||||
|
|
||||||
// Use absolute paths to ensure correct file location
|
// Use absolute paths to ensure correct file location
|
||||||
const ADMIN_STORAGE_STATE = path.join(__dirname, '.auth', 'admin.json');
|
const ADMIN_STORAGE_STATE = path.join(__dirname, '.auth', 'admin.json');
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export async function startCoverage(
|
|||||||
try {
|
try {
|
||||||
await page.coverage.startJSCoverage({
|
await page.coverage.startJSCoverage({
|
||||||
resetOnNavigation: options?.resetOnNavigation ?? false,
|
resetOnNavigation: options?.resetOnNavigation ?? false,
|
||||||
// @ts-ignore
|
// @ts-expect-error - includeRawScriptCoverage is not in official types but supported by Playwright
|
||||||
includeRawScriptCoverage: options?.includeRawScriptCoverage ?? false,
|
includeRawScriptCoverage: options?.includeRawScriptCoverage ?? false,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ test.describe('Homepage - Mobile Menu Interactions', () => {
|
|||||||
|
|
||||||
test.skip('should close mobile menu when clicking outside', async ({ page }) => {
|
test.skip('should close mobile menu when clicking outside', async ({ page }) => {
|
||||||
// Open mobile menu
|
// Open mobile menu
|
||||||
const mobileMenu = await openMobileMenu(page);
|
const _mobileMenu = await openMobileMenu(page);
|
||||||
|
|
||||||
// Press Escape key to close menu (more reliable than clicking overlay)
|
// Press Escape key to close menu (more reliable than clicking overlay)
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { setupAuthenticatedMocks, loginViaUI } from './helpers/auth';
|
import { setupAuthenticatedMocks } from './helpers/auth';
|
||||||
|
|
||||||
test.describe('Settings Navigation', () => {
|
test.describe('Settings Navigation', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { setupAuthenticatedMocks, loginViaUI } from './helpers/auth';
|
import { setupAuthenticatedMocks } from './helpers/auth';
|
||||||
|
|
||||||
test.describe('Password Change', () => {
|
test.describe('Password Change', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { setupAuthenticatedMocks, loginViaUI, MOCK_USER } from './helpers/auth';
|
import { setupAuthenticatedMocks, MOCK_USER } from './helpers/auth';
|
||||||
|
|
||||||
test.describe('Profile Settings', () => {
|
test.describe('Profile Settings', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const compat = new FlatCompat({
|
|||||||
baseDirectory: __dirname,
|
baseDirectory: __dirname,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default [
|
const eslintConfig = [
|
||||||
...compat.extends('next/core-web-vitals'),
|
...compat.extends('next/core-web-vitals'),
|
||||||
...compat.extends('next/typescript'),
|
...compat.extends('next/typescript'),
|
||||||
{
|
{
|
||||||
@@ -23,9 +23,12 @@ export default [
|
|||||||
'src/lib/api/generated/**',
|
'src/lib/api/generated/**',
|
||||||
'*.gen.ts',
|
'*.gen.ts',
|
||||||
'*.gen.tsx',
|
'*.gen.tsx',
|
||||||
|
'next-env.d.ts', // Auto-generated by Next.js
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// Base rules for source code
|
||||||
{
|
{
|
||||||
|
files: ['src/**/*.{ts,tsx}'],
|
||||||
rules: {
|
rules: {
|
||||||
// Enforce Dependency Injection pattern for auth store
|
// Enforce Dependency Injection pattern for auth store
|
||||||
// Components/hooks must use useAuth() from AuthContext, not useAuthStore directly
|
// Components/hooks must use useAuth() from AuthContext, not useAuthStore directly
|
||||||
@@ -46,4 +49,56 @@ export default [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Relaxed rules for test files
|
||||||
|
{
|
||||||
|
files: ['tests/**/*.{ts,tsx}', '**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off', // Test mocks often need any
|
||||||
|
'@typescript-eslint/no-require-imports': 'off', // Jest sometimes needs require
|
||||||
|
'react/display-name': 'off', // Mock components don't need display names
|
||||||
|
'no-restricted-imports': 'off', // Tests can import store directly
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
destructuredArrayIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Relaxed rules for E2E tests
|
||||||
|
{
|
||||||
|
files: ['e2e/**/*.{ts,tsx}'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off', // Playwright helpers need flexibility
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
destructuredArrayIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Relaxed rules for scripts and config files
|
||||||
|
{
|
||||||
|
files: ['scripts/**/*.{ts,js}', '*.config.{ts,js,mjs}', 'jest.setup.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off', // Build scripts need flexibility
|
||||||
|
'@typescript-eslint/no-require-imports': 'off', // CommonJS configs
|
||||||
|
'@next/next/no-assign-module-variable': 'off', // Scripts may need module.exports
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
destructuredArrayIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ async function convertV8ToIstanbul() {
|
|||||||
// Dynamic import to handle both scenarios (installed vs not installed)
|
// Dynamic import to handle both scenarios (installed vs not installed)
|
||||||
const module = await import('v8-to-istanbul');
|
const module = await import('v8-to-istanbul');
|
||||||
v8toIstanbul = module.default || module;
|
v8toIstanbul = module.default || module;
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.log('❌ v8-to-istanbul not installed\n');
|
console.log('❌ v8-to-istanbul not installed\n');
|
||||||
console.log('📦 Install it with:');
|
console.log('📦 Install it with:');
|
||||||
console.log(' npm install -D v8-to-istanbul\n');
|
console.log(' npm install -D v8-to-istanbul\n');
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import LoginPage from '@/app/(auth)/login/page';
|
|||||||
// Mock dynamic import
|
// Mock dynamic import
|
||||||
jest.mock('next/dynamic', () => ({
|
jest.mock('next/dynamic', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: (importFn: () => Promise<any>, options?: any) => {
|
default: (_importFn: () => Promise<any>, _options?: any) => {
|
||||||
const Component = () => <div data-testid="login-form">Mocked LoginForm</div>;
|
const Component = () => <div data-testid="login-form">Mocked LoginForm</div>;
|
||||||
Component.displayName = 'LoginForm';
|
Component.displayName = 'LoginForm';
|
||||||
return Component;
|
return Component;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jest.mock('next/link', () => ({
|
|||||||
// Mock dynamic import
|
// Mock dynamic import
|
||||||
jest.mock('next/dynamic', () => ({
|
jest.mock('next/dynamic', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: (importFn: () => Promise<any>, options?: any) => {
|
default: (_importFn: () => Promise<any>, _options?: any) => {
|
||||||
const Component = ({ onSuccess }: { onSuccess?: () => void }) => (
|
const Component = ({ onSuccess }: { onSuccess?: () => void }) => (
|
||||||
<div data-testid="password-reset-confirm-form">
|
<div data-testid="password-reset-confirm-form">
|
||||||
<button onClick={onSuccess}>Submit</button>
|
<button onClick={onSuccess}>Submit</button>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import PasswordResetPage from '@/app/(auth)/password-reset/page';
|
|||||||
// Mock dynamic import
|
// Mock dynamic import
|
||||||
jest.mock('next/dynamic', () => ({
|
jest.mock('next/dynamic', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: (importFn: () => Promise<any>, options?: any) => {
|
default: (_importFn: () => Promise<any>, _options?: any) => {
|
||||||
const Component = () => <div data-testid="password-reset-form">Mocked PasswordResetRequestForm</div>;
|
const Component = () => <div data-testid="password-reset-form">Mocked PasswordResetRequestForm</div>;
|
||||||
Component.displayName = 'PasswordResetRequestForm';
|
Component.displayName = 'PasswordResetRequestForm';
|
||||||
return Component;
|
return Component;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import RegisterPage from '@/app/(auth)/register/page';
|
|||||||
// Mock dynamic import
|
// Mock dynamic import
|
||||||
jest.mock('next/dynamic', () => ({
|
jest.mock('next/dynamic', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: (importFn: () => Promise<any>, options?: any) => {
|
default: (_importFn: () => Promise<any>, _options?: any) => {
|
||||||
const Component = () => <div data-testid="register-form">Mocked RegisterForm</div>;
|
const Component = () => <div data-testid="register-form">Mocked RegisterForm</div>;
|
||||||
Component.displayName = 'RegisterForm';
|
Component.displayName = 'RegisterForm';
|
||||||
return Component;
|
return Component;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Smoke tests for page rendering
|
* Smoke tests for page rendering
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import SessionsPage from '@/app/(authenticated)/settings/sessions/page';
|
import SessionsPage from '@/app/(authenticated)/settings/sessions/page';
|
||||||
|
|
||||||
|
|||||||
@@ -510,8 +510,6 @@ describe('UserActionMenu', () => {
|
|||||||
|
|
||||||
describe('User Name Display', () => {
|
describe('User Name Display', () => {
|
||||||
it('displays full name when last name is provided', async () => {
|
it('displays full name when last name is provided', async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<UserActionMenu
|
<UserActionMenu
|
||||||
user={mockUser}
|
user={mockUser}
|
||||||
@@ -527,7 +525,6 @@ describe('UserActionMenu', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('displays first name only when last name is null', async () => {
|
it('displays first name only when last name is null', async () => {
|
||||||
const user = userEvent.setup();
|
|
||||||
const userWithoutLastName = { ...mockUser, last_name: null };
|
const userWithoutLastName = { ...mockUser, last_name: null };
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ describe('UserFormDialog', () => {
|
|||||||
|
|
||||||
describe('Module Exports', () => {
|
describe('Module Exports', () => {
|
||||||
it('exports UserFormDialog component', () => {
|
it('exports UserFormDialog component', () => {
|
||||||
const module = require('@/components/admin/users/UserFormDialog');
|
const moduleExports = require('@/components/admin/users/UserFormDialog');
|
||||||
expect(module.UserFormDialog).toBeDefined();
|
expect(moduleExports.UserFormDialog).toBeDefined();
|
||||||
expect(typeof module.UserFormDialog).toBe('function');
|
expect(typeof moduleExports.UserFormDialog).toBe('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('component is a valid React component', () => {
|
it('component is a valid React component', () => {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ describe('HeaderSkeleton', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('has proper styling classes', () => {
|
it('has proper styling classes', () => {
|
||||||
const { container } = render(<HeaderSkeleton />);
|
render(<HeaderSkeleton />);
|
||||||
|
|
||||||
// Verify backdrop blur and background
|
// Verify backdrop blur and background
|
||||||
const header = screen.getByRole('banner');
|
const header = screen.getByRole('banner');
|
||||||
@@ -60,7 +60,7 @@ describe('AuthLoadingSkeleton', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders main content with container', () => {
|
it('renders main content with container', () => {
|
||||||
const { container } = render(<AuthLoadingSkeleton />);
|
render(<AuthLoadingSkeleton />);
|
||||||
|
|
||||||
const main = screen.getByRole('main');
|
const main = screen.getByRole('main');
|
||||||
expect(main).toHaveClass('flex-1');
|
expect(main).toHaveClass('flex-1');
|
||||||
@@ -71,7 +71,7 @@ describe('AuthLoadingSkeleton', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders skeleton placeholders in main content', () => {
|
it('renders skeleton placeholders in main content', () => {
|
||||||
const { container } = render(<AuthLoadingSkeleton />);
|
render(<AuthLoadingSkeleton />);
|
||||||
|
|
||||||
const main = screen.getByRole('main');
|
const main = screen.getByRole('main');
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Tests for PasswordChangeForm Component
|
* Tests for PasswordChangeForm Component
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { PasswordChangeForm } from '@/components/settings/PasswordChangeForm';
|
import { PasswordChangeForm } from '@/components/settings/PasswordChangeForm';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { ThemeToggle } from '@/components/theme/ThemeToggle';
|
import { ThemeToggle } from '@/components/theme/ThemeToggle';
|
||||||
import { ThemeProvider, useTheme } from '@/components/theme/ThemeProvider';
|
import { useTheme } from '@/components/theme/ThemeProvider';
|
||||||
|
|
||||||
// Mock theme provider for controlled testing
|
// Mock theme provider for controlled testing
|
||||||
jest.mock('@/components/theme/ThemeProvider', () => {
|
jest.mock('@/components/theme/ThemeProvider', () => {
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ describe('usePrefersReducedMotion', () => {
|
|||||||
it('handles SSR environment safely', () => {
|
it('handles SSR environment safely', () => {
|
||||||
const originalWindow = global.window;
|
const originalWindow = global.window;
|
||||||
|
|
||||||
// @ts-ignore - Simulating SSR
|
// @ts-expect-error - Simulating SSR
|
||||||
delete global.window;
|
delete global.window;
|
||||||
|
|
||||||
const { result } = renderHook(() => usePrefersReducedMotion());
|
const { result } = renderHook(() => usePrefersReducedMotion());
|
||||||
|
|||||||
@@ -630,7 +630,7 @@ describe('API Error Handling', () => {
|
|||||||
|
|
||||||
describe('Error message completeness', () => {
|
describe('Error message completeness', () => {
|
||||||
it('should have non-empty messages for all error codes', () => {
|
it('should have non-empty messages for all error codes', () => {
|
||||||
Object.entries(ERROR_MESSAGES).forEach(([code, message]) => {
|
Object.entries(ERROR_MESSAGES).forEach(([_code, message]) => {
|
||||||
expect(message).toBeTruthy();
|
expect(message).toBeTruthy();
|
||||||
expect(message.length).toBeGreaterThan(0);
|
expect(message.length).toBeGreaterThan(0);
|
||||||
expect(message).not.toBe('');
|
expect(message).not.toBe('');
|
||||||
@@ -639,7 +639,7 @@ describe('API Error Handling', () => {
|
|||||||
|
|
||||||
it('should have user-friendly messages', () => {
|
it('should have user-friendly messages', () => {
|
||||||
// Check that messages don't contain technical jargon or error codes
|
// Check that messages don't contain technical jargon or error codes
|
||||||
Object.entries(ERROR_MESSAGES).forEach(([code, message]) => {
|
Object.entries(ERROR_MESSAGES).forEach(([_code, message]) => {
|
||||||
expect(message).not.toContain('null');
|
expect(message).not.toContain('null');
|
||||||
expect(message).not.toContain('undefined');
|
expect(message).not.toContain('undefined');
|
||||||
expect(message).not.toContain('Error:');
|
expect(message).not.toContain('Error:');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* These tests cover hook setup, types, and basic integration
|
* These tests cover hook setup, types, and basic integration
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { renderHook, waitFor } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
useIsAuthenticated,
|
useIsAuthenticated,
|
||||||
|
|||||||
Reference in New Issue
Block a user