diff --git a/api/apiFunctions.interface.ts b/api/apiFunctions.interface.ts index ed8352ba..e5d1f6fb 100644 --- a/api/apiFunctions.interface.ts +++ b/api/apiFunctions.interface.ts @@ -9,6 +9,16 @@ export interface IAccountData { } export interface IUser { documentId: string; + // for the appwrite auth collection + id: string; + email: string; + leagues: string[]; + labels: string[]; +} + +export interface ICollectionUser { + documentId: string; + // for the custom user collection id: string; email: string; leagues: string[]; diff --git a/api/apiFunctions.ts b/api/apiFunctions.ts index 93041dd3..2f3b072d 100644 --- a/api/apiFunctions.ts +++ b/api/apiFunctions.ts @@ -8,6 +8,7 @@ import { ILeague, IGameWeek, IUser, + ICollectionUser, IWeeklyPicks, INFLTeam, IRecoveryToken, @@ -149,7 +150,9 @@ export async function updateUserEmail({ * @param userId - The user ID * @returns {Models.DocumentList | Error} - The user object or an error */ -export async function getCurrentUser(userId: IUser['id']): Promise { +export async function getCurrentUser( + userId: IUser['id'], +): Promise { try { const user = await databases.listDocuments( appwriteConfig.databaseId, diff --git a/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.test.tsx b/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.test.tsx index 78b69c1d..516631f6 100644 --- a/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.test.tsx +++ b/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.test.tsx @@ -186,6 +186,8 @@ describe('League Week Picks', () => { // Wait for the main content to be displayed await waitFor(() => { expect(screen.getByTestId('weekly-picks')).toBeInTheDocument(); + expect(screen.getByTestId('week__week-number')).toHaveTextContent('Week 1'); + expect(screen.getByTestId('week__entry-name')).toHaveTextContent('Entry 1'); }); expect(screen.queryByTestId('global-spinner')).not.toBeInTheDocument(); diff --git a/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.tsx b/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.tsx index cfbbf965..6edab010 100644 --- a/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.tsx +++ b/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.tsx @@ -34,6 +34,7 @@ import Image from 'next/image'; import { useRouter } from 'next/navigation'; import LinkCustom from '@/components/LinkCustom/LinkCustom'; import { ChevronLeft } from 'lucide-react'; +import Heading from '@/components/Heading/Heading'; /** * Renders the weekly picks page. @@ -43,6 +44,7 @@ import { ChevronLeft } from 'lucide-react'; // eslint-disable-next-line no-unused-vars const Week = ({ entry, league, NFLTeams, week }: IWeekProps): JSX.Element => { const [pickHistory, setPickHistory] = useState([]); + const [entryName, setEntryName] = useState(''); const [error, setError] = useState(null); const [schedule, setSchedule] = useState([]); const [selectedLeague, setSelectedLeague] = useState(); @@ -156,7 +158,8 @@ const Week = ({ entry, league, NFLTeams, week }: IWeekProps): JSX.Element => { if (!currentEntry) { throw new Error('Entry not found'); } - + + setEntryName(currentEntry.name); let entryHistory = currentEntry?.selectedTeams || []; if (currentEntry?.selectedTeams.length > 0) { @@ -271,10 +274,18 @@ const Week = ({ entry, league, NFLTeams, week }: IWeekProps): JSX.Element => { className="flex flex-col items-center w-full pt-8" data-testid="weekly-picks" > -

- Week {week} pick -

- + {`Week ${week} pick`} + + {entryName} + {pickHistory.length > 0 && (
({ id: '1234', email: 'test@test.com', leagues: ['league1'], + labels: [], }, allLeagues: [ { @@ -82,6 +83,7 @@ describe('Leagues Component', () => { leagues: [], }, allLeagues: [], + labels: [], }); render(); @@ -105,6 +107,7 @@ describe('Leagues Component', () => { leagues: [], }, allLeagues: [], + labels: [], }); render(); @@ -121,6 +124,7 @@ describe('Leagues Component', () => { email: 'test@test.com', id: '123', leagues: [], + labels: [], }, allLeagues: [ { @@ -150,6 +154,7 @@ describe('Leagues Component', () => { email: 'test@test.com', id: '123', leagues: [], + labels: [], }; const league = { @@ -202,6 +207,7 @@ describe('Leagues Component', () => { user.id, user.email, [...user.leagues, league.leagueId], + user.labels, ); expect(toast.custom).toHaveBeenCalledWith( { email: 'test@test.com', id: '123', leagues: [], + labels: [], }; const league = { diff --git a/app/(main)/league/all/page.tsx b/app/(main)/league/all/page.tsx index 56c04323..8e8a77de 100644 --- a/app/(main)/league/all/page.tsx +++ b/app/(main)/league/all/page.tsx @@ -100,10 +100,13 @@ const Leagues = (): JSX.Element => { }); setLeagues([...leagues, league]); - updateUser(user.documentId, user.id, user.email, [ - ...user.leagues, - league.leagueId, - ]); + updateUser( + user.documentId, + user.id, + user.email, + [...user.leagues, league.leagueId], + user.labels, + ); toast.custom( data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ['confirmPassword'], + }); + +type RegisterUserSchemaType = z.infer; + +/** + * Renders the registration page. + * @returns {JSX.Element} The rendered registration page. + */ +const Register = (): JSX.Element => { + const router = useRouter(); + const { login, isSignedIn } = useAuthContext(); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (isSignedIn) { + router.push('/league/all'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSignedIn]); + + const form = useForm({ + resolver: zodResolver(RegisterUserSchema), + }); + + /** + * The current value of the 'email' field in the form. + * @type {string} + */ + const email: string = useWatch({ + control: form.control, + name: 'email', + defaultValue: '', + }); + + /** + * The current value of the 'password' field in the form. + * @type {string} + */ + const password: string = useWatch({ + control: form.control, + name: 'password', + defaultValue: '', + }); + + /** + * The current value of the 'confirmPassword' field in the form. + * @type {string} + */ + const confirmPassword: string = useWatch({ + control: form.control, + name: 'confirmPassword', + defaultValue: '', + }); + + /** + * A function that handles form submission. + * @param {RegisterUserSchemaType} data - The data submitted in the form. + * @returns {Promise} Promise that resolves after form submission is processed. + */ + const onSubmit: SubmitHandler = async ( + data: RegisterUserSchemaType, + ): Promise => { + try { + setIsLoading(true); + await registerAccount(data); + await login(data); + toast.custom( + , + ); + } catch (error) { + console.error('Registration Failed', error); + toast.custom( + , + ); + } finally { + setIsLoading(false); + } + }; + + const isDisabled = !email || !password || password !== confirmPassword; + + return ( +
+
+ +
+

+ Thank you... fantasy football draft, for letting me know that even + in my fantasies, I am bad at sports. +

+

Jimmy Fallon

+
+
+
+
+

+ Register A New Account +

+

+ If you have an existing account{' '} + Login! +

+
+ +
+ + } + name="email" + render={({ field }) => ( + + + Email + + + + + {form.formState.errors?.email && ( + + {form.formState.errors?.email.message} + + )} + + )} + /> + } + name="password" + render={({ field }) => ( + + + Password + + + + + {form.formState.errors?.password && ( + + {form.formState.errors?.password.message} + + )} + + )} + /> + } + name="confirmPassword" + render={({ field }) => ( + + + Confirm Password + + + + + {form.formState.errors?.confirmPassword && ( + + {form.formState.errors?.confirmPassword.message} + + )} + + )} + /> + +
+
+ ); +}; + +export default Register; \ No newline at end of file diff --git a/app/(main)/register/page.test.tsx b/app/(main)/register/page.test.tsx index e03874c0..36d7d66a 100644 --- a/app/(main)/register/page.test.tsx +++ b/app/(main)/register/page.test.tsx @@ -5,7 +5,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { toast } from 'react-hot-toast'; import Alert from '@/components/AlertNotification/AlertNotification'; import React, { useState as useStateMock } from 'react'; -import Register from './page'; +import RegisterPage from './page'; const mockLogin = jest.fn(); const mockPush = jest.fn(); @@ -57,7 +57,7 @@ describe('Register', () => { .spyOn(React, 'useState') .mockImplementation(() => [false, setIsLoading]); - render(); + render(); confirmPasswordInput = screen.getByTestId('confirm-password'); continueButton = screen.getByTestId('continue-button'); @@ -98,7 +98,7 @@ describe('Register', () => { it('redirects to /league/all when the button is clicked', async () => { mockUseAuthContext.isSignedIn = true; - render(); + render(); await waitFor(() => { expect(mockPush).toHaveBeenCalledWith('/league/all'); @@ -177,7 +177,7 @@ describe('Register loading spinner', () => { setIsLoading, ]); - render(); + render(); await waitFor(() => { expect(screen.queryByTestId('loading-spinner')).toBeInTheDocument(); @@ -189,7 +189,7 @@ describe('Register loading spinner', () => { setIsLoading, ]); - render(); + render(); await waitFor(() => { expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument(); diff --git a/app/(main)/register/page.tsx b/app/(main)/register/page.tsx index 4d1b8e8d..32323ed3 100644 --- a/app/(main)/register/page.tsx +++ b/app/(main)/register/page.tsx @@ -1,248 +1,21 @@ // Copyright (c) Gridiron Survivor. // Licensed under the MIT License. -'use client'; -import { AlertVariants } from '@/components/AlertNotification/Alerts.enum'; -import { Button } from '@/components/Button/Button'; -import { Control, useForm, useWatch, SubmitHandler } from 'react-hook-form'; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from '../../../components/Form/Form'; -import { Input } from '@/components/Input/Input'; -import { registerAccount } from '@/api/apiFunctions'; -import { toast } from 'react-hot-toast'; -import { useAuthContext } from '@/context/AuthContextProvider'; -import { useRouter } from 'next/navigation'; -import { z } from 'zod'; -import { zodResolver } from '@hookform/resolvers/zod'; -import Alert from '@/components/AlertNotification/AlertNotification'; -import LinkCustom from '@/components/LinkCustom/LinkCustom'; -import Logo from '@/components/Logo/Logo'; -import logo from '/public/assets/logo-colored-outline.svg'; -import React, { JSX, useEffect, useState } from 'react'; -import LoadingSpinner from '@/components/LoadingSpinner/LoadingSpinner'; +import { JSX } from 'react'; +import { Metadata } from 'next'; +import Register from './Register'; -const RegisterUserSchema = z - .object({ - email: z - .string() - .min(1, { message: 'Please enter an email address' }) - .email({ message: 'Please enter a valid email address' }), - password: z - .string() - .min(1, { message: 'Please enter a password' }) - .min(8, { message: 'Password must be at least 8 characters' }), - confirmPassword: z - .string() - .min(1, { message: 'Please confirm your password' }) - .min(8, { message: 'Password must be at least 8 characters' }), - }) - .refine((data) => data.password === data.confirmPassword, { - message: "Passwords don't match", - path: ['confirmPassword'], - }); - -type RegisterUserSchemaType = z.infer; +export const metadata: Metadata = { + title: 'Registration | Gridiron Survivor', + description: 'Fantasy Football Survivor Pool', +}; /** * Renders the registration page. * @returns {JSX.Element} The rendered registration page. */ -const Register = (): JSX.Element => { - const router = useRouter(); - const { login, isSignedIn } = useAuthContext(); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - if (isSignedIn) { - router.push('/league/all'); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSignedIn]); - - const form = useForm({ - resolver: zodResolver(RegisterUserSchema), - }); - - /** - * The current value of the 'email' field in the form. - * @type {string} - */ - const email: string = useWatch({ - control: form.control, - name: 'email', - defaultValue: '', - }); - - /** - * The current value of the 'password' field in the form. - * @type {string} - */ - const password: string = useWatch({ - control: form.control, - name: 'password', - defaultValue: '', - }); - - /** - * The current value of the 'confirmPassword' field in the form. - * @type {string} - */ - const confirmPassword: string = useWatch({ - control: form.control, - name: 'confirmPassword', - defaultValue: '', - }); - - /** - * A function that handles form submission. - * @param {RegisterUserSchemaType} data - The data submitted in the form. - * @returns {Promise} Promise that resolves after form submission is processed. - */ - const onSubmit: SubmitHandler = async ( - data: RegisterUserSchemaType, - ): Promise => { - try { - setIsLoading(true); - await registerAccount(data); - await login(data); - toast.custom( - , - ); - } catch (error) { - console.error('Registration Failed', error); - toast.custom( - , - ); - } finally { - setIsLoading(false); - } - }; - - const isDisabled = !email || !password || password !== confirmPassword; - - return ( -
-
- -
-

- Thank you... fantasy football draft, for letting me know that even - in my fantasies, I am bad at sports. -

-

Jimmy Fallon

-
-
-
-
-

- Register A New Account -

-

- If you have an existing account{' '} - Login! -

-
- -
- - } - name="email" - render={({ field }) => ( - - - - - {form.formState.errors?.email && ( - - {form.formState.errors?.email.message} - - )} - - )} - /> - } - name="password" - render={({ field }) => ( - - - - - {form.formState.errors?.password && ( - - {form.formState.errors?.password.message} - - )} - - )} - /> - } - name="confirmPassword" - render={({ field }) => ( - - - - - {form.formState.errors?.confirmPassword && ( - - {form.formState.errors?.confirmPassword.message} - - )} - - )} - /> - -
-
- ); +const RegisterPage = (): JSX.Element => { + return ; }; - -export default Register; + +export default RegisterPage; \ No newline at end of file diff --git a/components/Label/Label.test.tsx b/components/Label/Label.test.tsx new file mode 100644 index 00000000..c9c77800 --- /dev/null +++ b/components/Label/Label.test.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { Label } from './Label'; + +describe('Label', () => { + const disabledVariants = [true, false]; + + disabledVariants.forEach((disabled) => { + it(`renders correctly when disabled is ${disabled}`, () => { + render( + + ); + + const label = screen.getByTestId('test-label'); + expect(label).toBeInTheDocument(); + + const expectedClass = disabled ? 'opacity-50 cursor-not-allowed' : 'peer-aria-checked:border-accent peer-hover:border-white'; + expect(label).toHaveClass(expectedClass); + }); + }); + + it('applies additional custom classes correctly', () => { + const extraClasses = 'custom-class-label extra-custom-class'; + render( + + ); + + const labelCustom = screen.getByTestId('custom-class-label'); + expect(labelCustom).toBeInTheDocument(); + expect(labelCustom).toHaveClass(extraClasses); + }); +}); + diff --git a/components/UpdateEmailForm/UpdateEmailForm.tsx b/components/UpdateEmailForm/UpdateEmailForm.tsx index 9348519c..c1316d89 100644 --- a/components/UpdateEmailForm/UpdateEmailForm.tsx +++ b/components/UpdateEmailForm/UpdateEmailForm.tsx @@ -82,7 +82,7 @@ const UpdateEmailForm = (): JSX.Element => { />, ); - updateUser(user.documentId, user.id, email, user.leagues); + updateUser(user.documentId, user.id, email, user.leagues, user.labels); form.reset({ email: email || '', password: '' }); } catch (error) { console.error('Email Update Failed', error); diff --git a/components/WeeklyPickButton/WeeklyPickButton.tsx b/components/WeeklyPickButton/WeeklyPickButton.tsx index f0dc9380..382c43f8 100644 --- a/components/WeeklyPickButton/WeeklyPickButton.tsx +++ b/components/WeeklyPickButton/WeeklyPickButton.tsx @@ -48,7 +48,12 @@ const WeeklyPickButton: React.FC = ({ data-testid="team-radio" /> -