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/app/(main)/league/[leagueId]/entry/all/page.tsx b/app/(main)/league/[leagueId]/entry/all/page.tsx index 36ba5ea4..43fe797f 100644 --- a/app/(main)/league/[leagueId]/entry/all/page.tsx +++ b/app/(main)/league/[leagueId]/entry/all/page.tsx @@ -205,10 +205,11 @@ const Entry = ({ return (
diff --git a/components/LeagueEntries/LeagueEntries.interface.ts b/components/LeagueEntries/LeagueEntries.interface.ts index f3462d48..13673cd6 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; + isLockedOutProp: boolean; teamLogo?: string; } diff --git a/components/LeagueEntries/LeagueEntries.test.tsx b/components/LeagueEntries/LeagueEntries.test.tsx index d9143508..4bd0bc44 100644 --- a/components/LeagueEntries/LeagueEntries.test.tsx +++ b/components/LeagueEntries/LeagueEntries.test.tsx @@ -4,39 +4,44 @@ import React from 'react'; describe('LeagueEntries', () => { 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); diff --git a/components/LeagueEntries/LeagueEntries.tsx b/components/LeagueEntries/LeagueEntries.tsx index 51026c16..a914969c 100644 --- a/components/LeagueEntries/LeagueEntries.tsx +++ b/components/LeagueEntries/LeagueEntries.tsx @@ -1,12 +1,11 @@ // Copyright (c) Gridiron Survivor. // Licensed under the MIT License. -import { Button } from '../Button/Button'; import { cn } from '@/utils/utils'; import { EntryStatus } from '../EntryStatus/EntryStatus'; import { ILeagueEntriesProps } from './LeagueEntries.interface'; -import React, { JSX } from 'react'; -import Link from 'next/link'; +import LinkCustom from '../LinkCustom/LinkCustom'; +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 @@ -16,67 +15,113 @@ import Link from 'next/link'; * @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.teamLogo - the team logo + * @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, - linkUrl, isEliminated = false, isPickSet = false, + linkUrl, + 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 currentDateAndTime = new Date(); + const day = currentDateAndTime.getUTCDay(); + const hours = currentDateAndTime.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 && ( - -
-
- -); + +
+ {!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.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(); diff --git a/components/LinkCustom/LinkCustom.tsx b/components/LinkCustom/LinkCustom.tsx index 868ae100..e2ad2f6f 100644 --- a/components/LinkCustom/LinkCustom.tsx +++ b/components/LinkCustom/LinkCustom.tsx @@ -3,35 +3,72 @@ 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; + dataTestidProp?: string; href: string; + // eslint-disable-next-line no-unused-vars + onClick?: (e: React.MouseEvent) => void; } /** * 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.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 + * @param props.variant - the variant of the link + * @returns {React.JSX.Element} - A link element */ const LinkCustom = ({ children, className, + dataTestidProp, href, + onClick, + size, + variant, }: ILinkCustomProps): JSX.Element => { return ( {children}