From 50256fe5595d040f036b301aae624b8c5f2c34b4 Mon Sep 17 00:00:00 2001 From: Ryan Furrer Date: Wed, 11 Sep 2024 22:49:05 -0400 Subject: [PATCH 1/8] start making lockout work --- .../league/[leagueId]/entry/all/page.tsx | 33 +++++++++++++++++++ .../LeagueEntries/LeagueEntries.interface.ts | 1 + components/LeagueEntries/LeagueEntries.tsx | 3 ++ 3 files changed, 37 insertions(+) diff --git a/app/(main)/league/[leagueId]/entry/all/page.tsx b/app/(main)/league/[leagueId]/entry/all/page.tsx index 36ba5ea4..de21fbd8 100644 --- a/app/(main)/league/[leagueId]/entry/all/page.tsx +++ b/app/(main)/league/[leagueId]/entry/all/page.tsx @@ -43,6 +43,38 @@ const Entry = ({ const { currentWeek, NFLTeams, user, updateCurrentWeek, updateNFLTeams } = useDataStore((state) => state); const MAX_ENTRIES = 5; + const [lockout, setLockout] = useState(false); + + useEffect(() => { + /** + * Checks if the current time is within the lockout period. + */ + const checkLockout = (): void => { + const now = new Date(); + const day = now.getUTCDay(); + const hours = now.getUTCHours(); + + // Lockout from 12am UTC on Thursday (day 4) until 12pm UTC on the following Tuesday (day 2) + if ( + (day === 4 && hours >= 0) || // Thursday from 12am + (day > 4 && day < 2) || // Friday, Saturday, Sunday, Monday + (day === 2 && hours < 12) // Tuesday until 12pm + ) { + setLockout(true); + } else { + setLockout(false); + } + }; + + // Check immediately on page load + checkLockout(); + + // Check time every hour + const intervalId = setInterval(checkLockout, 60 * 60 * 1000); + + // Cleanup interval on unmount + return (): void => clearInterval(intervalId); + }, []); useEffect(() => { /** @@ -210,6 +242,7 @@ const Entry = ({ isEliminated={entry.eliminated} isPickSet={isPickSet} linkUrl={linkUrl} + lockout={lockout} teamLogo={teamLogo} /> diff --git a/components/LeagueEntries/LeagueEntries.interface.ts b/components/LeagueEntries/LeagueEntries.interface.ts index f3462d48..703d0b40 100644 --- a/components/LeagueEntries/LeagueEntries.interface.ts +++ b/components/LeagueEntries/LeagueEntries.interface.ts @@ -6,5 +6,6 @@ export interface ILeagueEntriesProps { linkUrl: string; isEliminated?: boolean; isPickSet?: boolean; + lockout: boolean; teamLogo?: string; } diff --git a/components/LeagueEntries/LeagueEntries.tsx b/components/LeagueEntries/LeagueEntries.tsx index 51026c16..b12fd1ee 100644 --- a/components/LeagueEntries/LeagueEntries.tsx +++ b/components/LeagueEntries/LeagueEntries.tsx @@ -15,6 +15,7 @@ import Link from 'next/link'; * @param props.linkUrl - the url to the user's entry page * @param props.isEliminated - If true, the user is flagged as eliminat4ed * @param props.isPickSet - if true, the team logo of the picked team shows up on the LeagueEntries card and the button changes from "make a pick" to "chagne pick" + * @param props.lockout - if true, the user is locked out from making a pick * @param props.teamLogo - the team logo * @returns {React.JSX.Element} - A div element that contains the user's entry information */ @@ -23,6 +24,7 @@ const LeagueEntries = ({ linkUrl, isEliminated = false, isPickSet = false, + lockout = false, teamLogo = '', }: ILeagueEntriesProps): JSX.Element => (
From 5e399f6504e07b4cd40436102fca177aad52ad76 Mon Sep 17 00:00:00 2001 From: Ryan Furrer Date: Thu, 12 Sep 2024 15:58:29 -0400 Subject: [PATCH 2/8] rework of make pick/change pick button into CustomLink and expanded CustomLink to have necessary variants. --- .../league/[leagueId]/entry/all/page.tsx | 4 +- components/LeagueEntries/LeagueEntries.tsx | 26 +++++----- components/LinkCustom/LinkCustom.tsx | 51 +++++++++++++++---- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/app/(main)/league/[leagueId]/entry/all/page.tsx b/app/(main)/league/[leagueId]/entry/all/page.tsx index de21fbd8..0f37a0e5 100644 --- a/app/(main)/league/[leagueId]/entry/all/page.tsx +++ b/app/(main)/league/[leagueId]/entry/all/page.tsx @@ -66,7 +66,7 @@ const Entry = ({ } }; - // Check immediately on page load + // Check immediately on mount checkLockout(); // Check time every hour @@ -237,10 +237,10 @@ const Entry = ({ return (
( @@ -67,15 +66,16 @@ const LeagueEntries = ({ data-testid="league-entry-pick-button-container" > {!isEliminated && ( - -
diff --git a/components/LinkCustom/LinkCustom.tsx b/components/LinkCustom/LinkCustom.tsx index 868ae100..06454992 100644 --- a/components/LinkCustom/LinkCustom.tsx +++ b/components/LinkCustom/LinkCustom.tsx @@ -3,33 +3,62 @@ import Link from 'next/link'; import React, { JSX } from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; + import { cn } from '@/utils/utils'; -interface ILinkCustomProps { - children: React.ReactNode; +const linkCustomVariants = cva( + 'inline-flex items-center justify-center whitespace-nowrap rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed', + { + variants: { + variant: { + default: 'underline underline-offset-4 hover:text-primary-muted transition-colors', + primaryButton: 'bg-primary text-primary-foreground hover:bg-primary-muted text-sm font-medium', + disabledPrimaryButton: 'bg-primary text-primary-foreground hover:bg-primary-muted text-sm font-medium opacity-50 cursor-not-allowed', + secondaryButton: 'bg-secondary text-secondary-foreground hover:bg-secondary-muted text-sm font-medium', + disabledSecondaryButton: 'bg-secondary text-secondary-foreground hover:bg-secondary-muted text-sm font-medium opacity-50 cursor-not-allowed', + }, + size: { + default: 'h-fit w-fit', + defaultButton: 'h-10 px-4 py-2', + smButton: 'h-9 rounded-md px-3', + lgButton: 'h-11 rounded-md px-8', + icon: 'h-10 w-10', + } + }, + defaultVariants: { + size: 'default', + variant: 'default', + }, + } +); + +interface ILinkCustomProps extends VariantProps { + children?: React.ReactNode; className?: string; href: string; } /** * Custom link component - * @param props - The props - * @param props.children - any additional items you want inside the link. This could include things like the link text, icons, etc. - * @param props.href - this is the URL you want the link to point to - * @param props.className - any additional classes you want to add to that instance of the LinkCustom component. - * @returns The custom link component + * @param props - the props for LinkCustom + * @param props.children - the children of the link + * @param props.className - the class name of the link + * @param props.href - the url of the link + * @param props.size - the size of the link + * @param props.variant - the variant of the link + * @returns {React.JSX.Element} - A link element */ const LinkCustom = ({ children, className, href, + size, + variant, }: ILinkCustomProps): JSX.Element => { return ( Date: Fri, 13 Sep 2024 10:37:09 -0400 Subject: [PATCH 3/8] move checkLockout logic to LeagueEntries component --- .../league/[leagueId]/entry/all/page.tsx | 34 +--- .../LeagueEntries/LeagueEntries.interface.ts | 2 +- components/LeagueEntries/LeagueEntries.tsx | 149 +++++++++++------- components/LinkCustom/LinkCustom.tsx | 5 + 4 files changed, 102 insertions(+), 88 deletions(-) diff --git a/app/(main)/league/[leagueId]/entry/all/page.tsx b/app/(main)/league/[leagueId]/entry/all/page.tsx index 0f37a0e5..a5aaf1c4 100644 --- a/app/(main)/league/[leagueId]/entry/all/page.tsx +++ b/app/(main)/league/[leagueId]/entry/all/page.tsx @@ -43,38 +43,6 @@ const Entry = ({ const { currentWeek, NFLTeams, user, updateCurrentWeek, updateNFLTeams } = useDataStore((state) => state); const MAX_ENTRIES = 5; - const [lockout, setLockout] = useState(false); - - useEffect(() => { - /** - * Checks if the current time is within the lockout period. - */ - const checkLockout = (): void => { - const now = new Date(); - const day = now.getUTCDay(); - const hours = now.getUTCHours(); - - // Lockout from 12am UTC on Thursday (day 4) until 12pm UTC on the following Tuesday (day 2) - if ( - (day === 4 && hours >= 0) || // Thursday from 12am - (day > 4 && day < 2) || // Friday, Saturday, Sunday, Monday - (day === 2 && hours < 12) // Tuesday until 12pm - ) { - setLockout(true); - } else { - setLockout(false); - } - }; - - // Check immediately on mount - checkLockout(); - - // Check time every hour - const intervalId = setInterval(checkLockout, 60 * 60 * 1000); - - // Cleanup interval on unmount - return (): void => clearInterval(intervalId); - }, []); useEffect(() => { /** @@ -240,9 +208,9 @@ const Entry = ({ entryName={entry.name} isEliminated={entry.eliminated} isPickSet={isPickSet} + isLockedOutProp={false} key={entry.$id} linkUrl={linkUrl} - lockout={lockout} teamLogo={teamLogo} /> diff --git a/components/LeagueEntries/LeagueEntries.interface.ts b/components/LeagueEntries/LeagueEntries.interface.ts index 703d0b40..13673cd6 100644 --- a/components/LeagueEntries/LeagueEntries.interface.ts +++ b/components/LeagueEntries/LeagueEntries.interface.ts @@ -6,6 +6,6 @@ export interface ILeagueEntriesProps { linkUrl: string; isEliminated?: boolean; isPickSet?: boolean; - lockout: boolean; + isLockedOutProp: boolean; teamLogo?: string; } diff --git a/components/LeagueEntries/LeagueEntries.tsx b/components/LeagueEntries/LeagueEntries.tsx index 8f360453..25bd3f02 100644 --- a/components/LeagueEntries/LeagueEntries.tsx +++ b/components/LeagueEntries/LeagueEntries.tsx @@ -5,7 +5,7 @@ import { cn } from '@/utils/utils'; import { EntryStatus } from '../EntryStatus/EntryStatus'; import { ILeagueEntriesProps } from './LeagueEntries.interface'; import LinkCustom from '../LinkCustom/LinkCustom'; -import React, { JSX } from 'react'; +import React, { JSX, useEffect, useState } from 'react'; /** * A card that contains information on the user's entry for this league. Contains the entry number, their entry status (alive or eliminated), team logo once a pick is set, and a button to make a pick or change their pick @@ -18,68 +18,109 @@ import React, { JSX } from 'react'; * @param props.lockout - if true, the user is locked out from making a pick * @returns {React.JSX.Element} - A div element that contains the user's entry information */ + +/** + * Display all entries for a league. + * @param {string} leagueId - The league id. + * @returns {JSX.Element} The rendered entries component. + */ const LeagueEntries = ({ entryName, isEliminated = false, isPickSet = false, linkUrl, - lockout = false, + isLockedOutProp = false, teamLogo = '', -}: ILeagueEntriesProps): JSX.Element => ( -
-
{ + const [isLockedOut, setLockedOut] = useState(isLockedOutProp); + + useEffect(() => { + /** + * Checks if the user is locked out from making a pick + */ + const checkLockout = (): void => { + const now = new Date(); + const day = now.getUTCDay(); + const hours = now.getUTCHours(); + if ( + (day === 4 && hours >= 0) || + (day > 4 && day < 2) || + (day === 2 && hours < 12) + ) { + setLockedOut(true); + } else { + setLockedOut(false); + } + }; + + checkLockout(); + + const intervalId = setInterval(checkLockout, 60 * 60 * 1000); + + return (): void => clearInterval(intervalId); + }, []); + + return ( +
-

- {entryName} -

- -
-
- {isPickSet && ( - teamLogo - )} - -
+ {entryName} + + +
+
- {!isEliminated && ( - - {isPickSet ? 'Change Pick' : 'Make Pick'} - + {isPickSet && ( + teamLogo )} -
- - -); + +
+ {!isEliminated && ( + unknown }) => + isLockedOut === true && e.preventDefault() + } + size={'defaultButton'} + variant={isPickSet ? 'secondaryButton' : 'primaryButton'} + > + {isPickSet ? 'Change Pick' : 'Make Pick'} + + )} +
+ + + ); +}; export { LeagueEntries }; diff --git a/components/LinkCustom/LinkCustom.tsx b/components/LinkCustom/LinkCustom.tsx index 06454992..5fe3558e 100644 --- a/components/LinkCustom/LinkCustom.tsx +++ b/components/LinkCustom/LinkCustom.tsx @@ -37,6 +37,8 @@ interface ILinkCustomProps extends VariantProps { children?: React.ReactNode; className?: string; href: string; + // eslint-disable-next-line no-unused-vars + onClick?: (e: React.MouseEvent) => void; } /** @@ -45,6 +47,7 @@ interface ILinkCustomProps extends VariantProps { * @param props.children - the children of the link * @param props.className - the class name of the link * @param props.href - the url of the link + * @param props.onClick - the click event of the link * @param props.size - the size of the link * @param props.variant - the variant of the link * @returns {React.JSX.Element} - A link element @@ -53,6 +56,7 @@ const LinkCustom = ({ children, className, href, + onClick, size, variant, }: ILinkCustomProps): JSX.Element => { @@ -61,6 +65,7 @@ const LinkCustom = ({ className={cn(linkCustomVariants({ size, variant }), className)} data-testid="linkCustom" href={href} + onClick={onClick} passHref > {children} From 796689f6415697b9c1f1f3ebb7440b31c2910f86 Mon Sep 17 00:00:00 2001 From: Ryan Furrer Date: Fri, 13 Sep 2024 12:27:06 -0400 Subject: [PATCH 4/8] begin working on updating tests --- app/(main)/league/[leagueId]/entry/all/page.test.tsx | 8 ++++---- components/LeagueEntries/LeagueEntries.tsx | 2 +- components/LinkCustom/LinkCustom.tsx | 5 ++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/(main)/league/[leagueId]/entry/all/page.test.tsx b/app/(main)/league/[leagueId]/entry/all/page.test.tsx index d8ad0385..46981598 100644 --- a/app/(main)/league/[leagueId]/entry/all/page.test.tsx +++ b/app/(main)/league/[leagueId]/entry/all/page.test.tsx @@ -280,7 +280,7 @@ describe('League entries page (Entry Component)', () => { screen.queryByTestId('add-new-entry-button'), ).not.toBeInTheDocument(); }); - it('should display "Make Pick" button when no pick is set', async () => { + it('should display "Make Pick" link when no pick is set', async () => { mockGetCurrentUserEntries.mockResolvedValueOnce([ { $id: '123', @@ -293,13 +293,13 @@ describe('League entries page (Entry Component)', () => { render(); await waitFor(() => { - expect(screen.getByTestId('league-entry-pick-button')).toHaveTextContent( + expect(screen.getByTestId('league-entry-pick-link')).toHaveTextContent( 'Make Pick', ); }); }); - it('should render team logo and change button to "Change Pick" when a pick is made', async () => { + it('should render team logo and change link to "Change Pick" when a pick is made', async () => { mockUseDataStore.mockReturnValue({ ...mockUseDataStore(), currentWeek: 1, @@ -321,7 +321,7 @@ describe('League entries page (Entry Component)', () => { 'team-a-logo.png', ); - expect(screen.getByTestId('league-entry-pick-button')).toHaveTextContent( + expect(screen.getByTestId('league-entry-pick-link')).toHaveTextContent( 'Change Pick', ); }); diff --git a/components/LeagueEntries/LeagueEntries.tsx b/components/LeagueEntries/LeagueEntries.tsx index 25bd3f02..a3ca1566 100644 --- a/components/LeagueEntries/LeagueEntries.tsx +++ b/components/LeagueEntries/LeagueEntries.tsx @@ -106,7 +106,7 @@ const LeagueEntries = ({ className={ isLockedOut === true ? 'opacity-50 cursor-not-allowed' : '' } - data-testid="league-entry-pick-button-link" + data-testid="league-entry-pick-link" href={linkUrl} onClick={(e: { preventDefault: () => unknown }) => isLockedOut === true && e.preventDefault() diff --git a/components/LinkCustom/LinkCustom.tsx b/components/LinkCustom/LinkCustom.tsx index 5fe3558e..e2ad2f6f 100644 --- a/components/LinkCustom/LinkCustom.tsx +++ b/components/LinkCustom/LinkCustom.tsx @@ -36,6 +36,7 @@ const linkCustomVariants = cva( interface ILinkCustomProps extends VariantProps { children?: React.ReactNode; className?: string; + dataTestidProp?: string; href: string; // eslint-disable-next-line no-unused-vars onClick?: (e: React.MouseEvent) => void; @@ -46,6 +47,7 @@ interface ILinkCustomProps extends VariantProps { * @param props - the props for LinkCustom * @param props.children - the children of the link * @param props.className - the class name of the link + * @param props.dataTestidProp - the data-testid of the link * @param props.href - the url of the link * @param props.onClick - the click event of the link * @param props.size - the size of the link @@ -55,6 +57,7 @@ interface ILinkCustomProps extends VariantProps { const LinkCustom = ({ children, className, + dataTestidProp, href, onClick, size, @@ -63,7 +66,7 @@ const LinkCustom = ({ return ( Date: Tue, 17 Sep 2024 19:26:15 -0400 Subject: [PATCH 5/8] fix linkCustom test --- components/LinkCustom/LinkCustom.test.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/LinkCustom/LinkCustom.test.tsx b/components/LinkCustom/LinkCustom.test.tsx index 88686aa6..20421d2d 100644 --- a/components/LinkCustom/LinkCustom.test.tsx +++ b/components/LinkCustom/LinkCustom.test.tsx @@ -5,7 +5,11 @@ import LinkCustom from './LinkCustom'; describe('LinkCustom Component', () => { it('renders with default props', () => { render( - , + , ); const link = screen.getByTestId('linkCustom'); expect(link).toBeInTheDocument(); From 5c326ec0e79f23c48f32cfb8e30fa5184f22ce72 Mon Sep 17 00:00:00 2001 From: Ryan Furrer Date: Tue, 17 Sep 2024 19:51:34 -0400 Subject: [PATCH 6/8] add prop to customlink for data-test-id and fix a test on the league entries page.test --- components/LeagueEntries/LeagueEntries.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/components/LeagueEntries/LeagueEntries.tsx b/components/LeagueEntries/LeagueEntries.tsx index a3ca1566..5e0d8f93 100644 --- a/components/LeagueEntries/LeagueEntries.tsx +++ b/components/LeagueEntries/LeagueEntries.tsx @@ -103,10 +103,11 @@ const LeagueEntries = ({ {!isEliminated && ( unknown }) => isLockedOut === true && e.preventDefault() From 335b30bbb0cdc28f1163da6f78367960d12a2d87 Mon Sep 17 00:00:00 2001 From: Ryan Furrer Date: Tue, 17 Sep 2024 20:02:36 -0400 Subject: [PATCH 7/8] Fix LeagueEntries test --- .../league/[leagueId]/entry/all/page.tsx | 2 +- .../LeagueEntries/LeagueEntries.test.tsx | 35 +++++++++++-------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/app/(main)/league/[leagueId]/entry/all/page.tsx b/app/(main)/league/[leagueId]/entry/all/page.tsx index a5aaf1c4..43fe797f 100644 --- a/app/(main)/league/[leagueId]/entry/all/page.tsx +++ b/app/(main)/league/[leagueId]/entry/all/page.tsx @@ -207,8 +207,8 @@ const Entry = ({ { it(`renders 'default' state without a pick made`, () => { - render(); + render( + , + ); const leagueEntryContainerCard = screen.getByTestId( 'league-entry-container-card', ); const leagueEntryNumber = screen.getByTestId('league-entry-number'); const entryStatus = screen.getByTestId('entry-status'); - const leagueEntryPickButton = screen.getByTestId( - 'league-entry-pick-button', - ); + const leagueEntryPickLink = screen.getByTestId('league-entry-pick-link'); expect(entryStatus).toHaveTextContent('alive'); expect(leagueEntryContainerCard).toBeInTheDocument(); expect(leagueEntryNumber).toHaveTextContent('Entry 1'); - expect(leagueEntryPickButton).toHaveTextContent('Make Pick'); + expect(leagueEntryPickLink).toHaveTextContent('Make Pick'); }); it('renders as if the user made a pick', () => { - render(); + render( + , + ); const leagueEntryContainerCard = screen.getByTestId( 'league-entry-container-card', ); const leagueEntryNumber = screen.getByTestId('league-entry-number'); const entryStatus = screen.getByTestId('entry-status'); - const leagueEntryPickButton = screen.getByTestId( - 'league-entry-pick-button', - ); + const leagueEntryPickLink = screen.getByTestId('league-entry-pick-link'); expect(entryStatus).toHaveTextContent('alive'); expect(leagueEntryContainerCard).toBeInTheDocument(); expect(leagueEntryNumber).toHaveTextContent('Entry 2'); - expect(leagueEntryPickButton).toHaveTextContent('Change Pick'); + expect(leagueEntryPickLink).toHaveTextContent('Change Pick'); }); it('renders as if the user is eliminated', () => { @@ -44,6 +49,7 @@ describe('LeagueEntries', () => { , @@ -67,6 +73,7 @@ describe('LeagueEntries', () => { render( { ); const leagueEntryNumber = screen.getByTestId('league-entry-number'); const entryStatus = screen.getByTestId('entry-status'); - const leagueEntryPickButton = screen.getByTestId( - 'league-entry-pick-button', - ); - const leagueLink = screen.getByTestId('league-entry-pick-button-link'); + const leagueEntryPickLink = screen.getByTestId('league-entry-pick-link'); + const leagueLink = screen.getByTestId('league-entry-pick-link'); const leagueEntryLogo = screen.getByTestId('league-entry-logo'); expect(entryStatus).toHaveTextContent('alive'); expect(leagueEntryNumber).toHaveTextContent('Entry 2'); - expect(leagueEntryPickButton).toHaveTextContent('Change Pick'); + expect(leagueEntryPickLink).toHaveTextContent('Change Pick'); expect(leagueLink).toHaveAttribute('href', linkUrl); expect(leagueEntryLogo).toBeInTheDocument(); expect(leagueEntryLogo).toHaveAttribute('src', teamLogoUrl); From 6ea046c0be3364616cfa9b9d8007f4e299103c68 Mon Sep 17 00:00:00 2001 From: Ryan Furrer Date: Tue, 17 Sep 2024 20:11:51 -0400 Subject: [PATCH 8/8] update now variable name to currentDateAndTime --- components/LeagueEntries/LeagueEntries.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/LeagueEntries/LeagueEntries.tsx b/components/LeagueEntries/LeagueEntries.tsx index 5e0d8f93..a914969c 100644 --- a/components/LeagueEntries/LeagueEntries.tsx +++ b/components/LeagueEntries/LeagueEntries.tsx @@ -39,9 +39,9 @@ const LeagueEntries = ({ * Checks if the user is locked out from making a pick */ const checkLockout = (): void => { - const now = new Date(); - const day = now.getUTCDay(); - const hours = now.getUTCHours(); + const currentDateAndTime = new Date(); + const day = currentDateAndTime.getUTCDay(); + const hours = currentDateAndTime.getUTCHours(); if ( (day === 4 && hours >= 0) || (day > 4 && day < 2) ||