diff --git a/.env.example b/.env.example
deleted file mode 100644
index 27599447..00000000
--- a/.env.example
+++ /dev/null
@@ -1,4 +0,0 @@
-NEXT_PUBLIC_APPWRITE_API_URL=
-NEXT_PUBLIC_APPWRITE_PROJECT_ID=
-NEXT_PUBLIC_APPWRITE_DATABASE_ID=
-APPWRITE_API_KEY=
\ No newline at end of file
diff --git a/.storybook/main.ts b/.storybook/main.ts
index 04d7722b..691fc39c 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -2,6 +2,7 @@ import type { StorybookConfig } from '@storybook/nextjs';
const config: StorybookConfig = {
stories: [
+ '../stories/*.mdx',
'../stories/**/*.mdx',
'../components/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
@@ -12,6 +13,7 @@ const config: StorybookConfig = {
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
+ '@storybook/manager-api',
],
framework: {
name: '@storybook/nextjs',
diff --git a/.storybook/manager.js b/.storybook/manager.js
new file mode 100644
index 00000000..e73de14d
--- /dev/null
+++ b/.storybook/manager.js
@@ -0,0 +1,8 @@
+import { addons } from '@storybook/manager-api';
+
+addons.setConfig({
+ sidebar: {
+ showRoots: true,
+ collapsedRoots: ['about', 'technical-planning-documents'],
+ },
+});
diff --git a/.storybook/preview.ts b/.storybook/preview.ts
index 40b5e67a..9de16511 100644
--- a/.storybook/preview.ts
+++ b/.storybook/preview.ts
@@ -1,5 +1,6 @@
import type { Preview } from '@storybook/react';
import '../app/globals.css';
+import '../stories/styles.css';
const preview: Preview = {
parameters: {
@@ -9,13 +10,7 @@ const preview: Preview = {
date: /Date$/i,
},
},
- backgrounds: {
- default: 'dark',
- values: [
- { name: 'dark', value: '#09090B' },
- { name: 'light', value: '#fff' },
- ],
- },
+ layout: 'centered',
},
};
diff --git a/app/(main)/league/all/page.test.tsx b/app/(main)/league/all/page.test.tsx
index c38f4503..b1a783c8 100644
--- a/app/(main)/league/all/page.test.tsx
+++ b/app/(main)/league/all/page.test.tsx
@@ -1,107 +1,183 @@
-import { render, screen, waitFor, fireEvent } from '@testing-library/react';
+import {
+ render,
+ screen,
+ waitFor,
+ waitForElementToBeRemoved,
+ fireEvent,
+} from '@testing-library/react';
import Leagues from './page';
import { useDataStore } from '@/store/dataStore';
import { getUserLeagues } from '@/utils/utils';
-import {
- getGameWeek,
- getAllLeagues,
- getUserDocumentId,
-} from '@/api/apiFunctions';
-import { AuthContext } from '@/context/AuthContextProvider';
+import { getAllLeagues, addUserToLeague } from '@/api/apiFunctions';
+import { toast } from 'react-hot-toast';
+import Alert from '@/components/AlertNotification/AlertNotification';
+import { AlertVariants } from '@/components/AlertNotification/Alerts.enum';
-jest.mock('@/store/dataStore', () => ({
- useDataStore: jest.fn(() => ({ user: { id: '123', leagues: [] } })),
-}));
+const mockUseAuthContext = {
+ isSignedIn: false,
+};
-jest.mock('@/utils/utils', () => ({
- getUserLeagues: jest.fn(() => Promise.resolve([])),
+jest.mock('@/context/AuthContextProvider', () => ({
+ useAuthContext() {
+ return {
+ ...mockUseAuthContext,
+ };
+ },
}));
-jest.mock('@/api/apiFunctions', () => ({
- getGameWeek: jest.fn(() =>
- Promise.resolve({
- week: 1,
- }),
- ),
- getAllLeagues: jest.fn(() =>
- Promise.resolve([
+jest.mock('@/store/dataStore', () => ({
+ useDataStore: jest.fn(() => ({
+ user: {
+ documentId: '123',
+ id: '1234',
+ email: 'test@test.com',
+ leagues: ['league1'],
+ },
+ allLeagues: [
{
leagueId: '123',
leagueName: 'Test League',
- logo: 'https://example.com/logo.png',
- participants: ['123456', '78', '9'],
+ logo: 'logo.png',
+ participants: ['123456', '78'],
survivors: ['123456', '78'],
},
- ]),
- ),
+ ],
+ updateUser: jest.fn(),
+ })),
}));
-jest.mock('@/context/AuthContextProvider', () => ({
- useAuthContext: jest.fn(() => ({ user: { id: '123' } })),
+jest.mock('@/utils/utils', () => ({
+ getUserLeagues: jest.fn(() => Promise.resolve([])),
+ cn: jest.fn(),
+}));
+
+jest.mock('@/api/apiFunctions', () => ({
+ getAllLeagues: jest.fn(),
+ addUserToLeague: jest.fn(),
+}));
+
+jest.mock('react-hot-toast', () => ({
+ toast: {
+ custom: jest.fn(),
+ },
}));
describe('Leagues Component', () => {
const mockUseDataStore = useDataStore as unknown as jest.Mock;
const mockGetUserLeagues = getUserLeagues as jest.Mock;
- const mockGetGameWeek = getGameWeek as jest.Mock;
const mockGetAllLeagues = getAllLeagues as jest.Mock;
+ const mockAddUserToLeague = addUserToLeague as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
});
- xtest('should render "You are not enrolled in any leagues" message when no leagues are found', async () => {
- mockUseDataStore.mockReturnValueOnce({ user: { id: '123', leagues: [] } });
- mockGetUserLeagues.mockResolvedValueOnce([]);
- mockGetGameWeek.mockResolvedValueOnce({ week: 1 });
+ it('should render "You are not enrolled in any leagues" message when no leagues are found', async () => {
+ mockUseAuthContext.isSignedIn = true;
+ mockUseDataStore.mockReturnValue({
+ user: {
+ documentId: '123',
+ email: 'test@test.com',
+ id: '123',
+ leagues: [],
+ },
+ allLeagues: [],
+ });
+
render();
+ await waitForElementToBeRemoved(() => screen.getByTestId('global-spinner'));
+
await waitFor(() => {
- expect(
- screen.getByText('You are not enrolled in any leagues'),
- ).toBeInTheDocument();
+ const messageElement = screen.getByTestId('no-leagues-message');
+ expect(messageElement).toBeInTheDocument();
});
});
- xtest('should display GlobalSpinner while loading data', async () => {
- mockUseDataStore.mockReturnValueOnce({ user: { id: '123', leagues: [] } });
- mockGetUserLeagues.mockResolvedValueOnce([]);
- mockGetGameWeek.mockResolvedValueOnce({ week: 1 });
- render();
+ it('should display GlobalSpinner while loading data', async () => {
+ mockUseAuthContext.isSignedIn = true;
- await waitFor(() => {
- expect(screen.getByTestId('global-spinner')).toBeInTheDocument();
+ mockUseDataStore.mockReturnValueOnce({
+ user: {
+ documentId: '123',
+ email: 'test@test.com',
+ id: '123',
+ leagues: [],
+ },
+ allLeagues: [],
});
- });
- xtest('should not display GlobalSpinner after loading data', async () => {
- mockUseDataStore.mockReturnValueOnce({ user: { id: '123', leagues: [] } });
- mockGetUserLeagues.mockResolvedValueOnce([]);
- mockGetGameWeek.mockResolvedValueOnce({ week: 1 });
render();
expect(screen.getByTestId('global-spinner')).toBeInTheDocument();
-
- await waitFor(() => {
- expect(screen.queryByTestId('global-spinner')).not.toBeInTheDocument();
- });
});
- xtest('should handle form submission to join a league', async () => {
- mockUseDataStore.mockReturnValueOnce({
- user: { id: '123', leagues: [] },
+ it('should not display GlobalSpinner after loading data', async () => {
+ mockUseAuthContext.isSignedIn = true;
+
+ mockUseDataStore.mockReturnValue({
+ user: {
+ documentId: '123',
+ email: 'test@test.com',
+ id: '123',
+ leagues: [],
+ },
allLeagues: [
{
leagueId: '123',
leagueName: 'Test League',
- logo: 'https://findmylogo.com/logo.png',
+ logo: 'logo.png',
participants: ['123456', '78'],
- survivors: ['123456', '78', '9'],
+ survivors: ['123456', '78'],
},
],
});
+
mockGetUserLeagues.mockResolvedValueOnce([]);
- mockGetGameWeek.mockResolvedValueOnce({ week: 1 });
+
+ render();
+
+ await waitForElementToBeRemoved(() => screen.getByTestId('global-spinner'));
+
+ expect(screen.queryByTestId('global-spinner')).not.toBeInTheDocument();
+ });
+
+ it('should handle form submission to join a league', async () => {
+ mockUseAuthContext.isSignedIn = true;
+
+ const user = {
+ documentId: '123',
+ email: 'test@test.com',
+ id: '123',
+ leagues: [],
+ };
+
+ const league = {
+ leagueId: '123',
+ leagueName: 'Test League',
+ logo: 'logo.png',
+ participants: [],
+ survivors: [],
+ };
+
+ const updateUser = jest.fn();
+
+ mockUseDataStore.mockReturnValue({
+ user,
+ allLeagues: [league],
+ updateUser,
+ });
+
+ mockGetAllLeagues.mockResolvedValueOnce([league]);
+ mockAddUserToLeague.mockResolvedValue(
+ Promise.resolve({
+ userDocumentId: user.documentId,
+ selectedLeague: league.leagueId,
+ selectedLeagues: [league.leagueId],
+ participants: [user.id],
+ survivors: [user.id],
+ }),
+ );
render();
@@ -109,16 +185,93 @@ describe('Leagues Component', () => {
expect(screen.queryByTestId('global-spinner')).not.toBeInTheDocument();
});
- const selectElement = screen.getByLabelText(/Select league to join/i);
- expect(selectElement).toBeInTheDocument();
+ const selectElement = screen.getByTestId('select-available-leagues');
+ fireEvent.change(selectElement, { target: { value: '123' } });
+ fireEvent.click(screen.getByTestId('join-league-button'));
+
+ await waitFor(() => {
+ expect(mockAddUserToLeague).toHaveBeenCalledWith({
+ userDocumentId: user.documentId,
+ selectedLeague: league.leagueId,
+ selectedLeagues: [league.leagueId],
+ participants: [user.id],
+ survivors: [user.id],
+ });
+ expect(updateUser).toHaveBeenCalledWith(
+ user.documentId,
+ user.id,
+ user.email,
+ [...user.leagues, league.leagueId],
+ );
+ expect(toast.custom).toHaveBeenCalledWith(
+ ,
+ );
+ });
+ });
+
+ it('should show error if adding to league fails', async () => {
+ mockUseAuthContext.isSignedIn = true;
+
+ const user = {
+ documentId: '123',
+ email: 'test@test.com',
+ id: '123',
+ leagues: [],
+ };
+ const league = {
+ leagueId: '123',
+ leagueName: 'Test League',
+ logo: 'logo.png',
+ participants: [],
+ survivors: [],
+ };
+
+ mockUseDataStore.mockReturnValue({
+ user,
+ allLeagues: [league],
+ });
+
+ mockGetUserLeagues.mockResolvedValueOnce([]);
+ mockGetAllLeagues.mockResolvedValueOnce([league]);
+ mockAddUserToLeague.mockResolvedValue(
+ Promise.resolve({
+ userDocumentId: user.documentId,
+ selectedLeague: league.leagueId,
+ selectedLeagues: [league.leagueId],
+ participants: [user.id],
+ survivors: [user.id],
+ }),
+ );
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('global-spinner')).not.toBeInTheDocument();
+ });
+
+ const selectElement = screen.getByTestId('select-available-leagues');
fireEvent.change(selectElement, { target: { value: '123' } });
- fireEvent.click(screen.getByText(/Join League/i));
+ fireEvent.click(screen.getByTestId('join-league-button'));
await waitFor(() => {
- expect(
- screen.getByText('Added Test League to your leagues!'),
- ).toBeInTheDocument();
+ expect(mockAddUserToLeague).toHaveBeenCalledWith({
+ userDocumentId: user.documentId,
+ selectedLeague: league.leagueId,
+ selectedLeagues: [league.leagueId],
+ participants: [user.id],
+ survivors: [user.id],
+ });
+
+ expect(toast.custom).toHaveBeenCalledWith(
+ ,
+ );
});
});
});
diff --git a/app/(main)/league/all/page.tsx b/app/(main)/league/all/page.tsx
index 3e9e78dd..56c04323 100644
--- a/app/(main)/league/all/page.tsx
+++ b/app/(main)/league/all/page.tsx
@@ -124,13 +124,12 @@ const Leagues = (): JSX.Element => {
return (
{loadingData ? (
-
+
) : (
<>
Your Leagues
-
{leagues.length > 0 ? (
leagues.map((league) => (
@@ -145,7 +144,10 @@ const Leagues = (): JSX.Element => {
))
) : (
-
+
You are not enrolled in any leagues
@@ -169,6 +171,7 @@ const Leagues = (): JSX.Element => {
render={({ field, fieldState }) => (
<>
-
+
>
)}
diff --git a/components/Nav/Nav.test.tsx b/components/Nav/Nav.test.tsx
index 47abd352..b23ea7d9 100644
--- a/components/Nav/Nav.test.tsx
+++ b/components/Nav/Nav.test.tsx
@@ -1,6 +1,8 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import Nav from './Nav';
import Login from '@/app/(main)/login/page';
+import { useDataStore } from '@/store/dataStore';
+import { getUserLeagues } from '@/utils/utils';
const mockPush = jest.fn();
const mockUsePathname = jest.fn();
@@ -32,6 +34,10 @@ jest.mock('../../context/AuthContextProvider', () => ({
},
}));
+jest.mock('@/store/dataStore', () => ({
+ useDataStore: jest.fn(() => ({ user: { id: '123', leagues: [] } })),
+}));
+
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
@@ -47,10 +53,25 @@ Object.defineProperty(window, 'matchMedia', {
});
describe('Nav', () => {
+ const mockUseDataStore = useDataStore as unknown as jest.Mock;
+ const mockGetUserLeagues = getUserLeagues as jest.Mock;
+
beforeEach(() => {
jest.clearAllMocks();
});
+ it('renders link to /league/all', async () => {
+ render();
+
+ const drawTrigger = screen.getByTestId('drawer-trigger');
+ fireEvent.click(drawTrigger);
+
+ let linkNav: HTMLElement;
+ linkNav = await screen.getByTestId('league-link');
+ expect(linkNav).toBeInTheDocument();
+ expect(linkNav).toHaveAttribute('href', '/league/all');
+ });
+
it('it should render the default component state', () => {
mockUsePathname.mockImplementation(() => '/weeklyPicks');
@@ -163,4 +184,22 @@ describe('Nav', () => {
expect(drawerTrigger.getAttribute('data-state')).toBe('closed');
});
});
+
+ it('it should close the drawer when the leagues link is clicked', async () => {
+ mockUsePathname.mockImplementation(() => '/league/all');
+
+ render();
+
+ const drawerTrigger = screen.getByTestId('drawer-trigger');
+
+ fireEvent.click(drawerTrigger);
+
+ const linkNav = screen.getByTestId('league-link');
+
+ fireEvent.click(linkNav);
+
+ await waitFor(() => {
+ expect(drawerTrigger.getAttribute('data-state')).toBe('closed');
+ });
+ });
});
diff --git a/components/Nav/Nav.tsx b/components/Nav/Nav.tsx
index 47ed1bd0..9eba9c7e 100644
--- a/components/Nav/Nav.tsx
+++ b/components/Nav/Nav.tsx
@@ -6,6 +6,7 @@ import React, { JSX } from 'react';
import LogoNav from '../LogoNav/LogoNav';
import { Menu } from 'lucide-react';
import { Button } from '../Button/Button';
+import Link from 'next/link';
import {
Drawer,
DrawerContent,
@@ -68,9 +69,21 @@ export const Nav = (): JSX.Element => {
Gridiron Survivor
+ -
+ setOpen(false)}
+ >
+ Leagues
+
+
-