diff --git a/api/apiFunctions.test.tsx b/api/apiFunctions.test.tsx index 7be6a988..7863d1e1 100644 --- a/api/apiFunctions.test.tsx +++ b/api/apiFunctions.test.tsx @@ -1,9 +1,9 @@ -import { mock } from 'node:test'; import { recoverPassword, registerAccount, resetPassword, resetRecoveredPassword, + updateEntryName, updateUserEmail, } from './apiFunctions'; import { IUser } from './apiFunctions.interface'; @@ -24,6 +24,7 @@ jest.mock('./apiFunctions', () => { getAllLeagues: jest.fn(), getUserDocumentId: jest.fn(), addUserToLeague: jest.fn(), + updateEntryName: jest.fn(), }; }); @@ -497,4 +498,28 @@ describe('apiFunctions', () => { ); }); }); + + describe('update entry name', () => { + const mockEntryId = 'entry123'; + const mockEntryName = 'New Entry Name'; + const mockUpdatedEntry = { + $id: mockEntryId, + name: mockEntryName, + user: 'user123', + league: 'league123', + selectedTeams: [], + eliminated: false, + }; + + it('should successfully updateEntryName', async () => { + apiFunctions.updateEntryName.mockResolvedValue(mockUpdatedEntry); + + const result = await apiFunctions.updateEntryName({ + entryId: mockEntryId, + entryName: mockEntryName, + }); + + expect(result).toEqual(mockUpdatedEntry); + }); + }); }); diff --git a/api/apiFunctions.ts b/api/apiFunctions.ts index 2f3b072d..cc5ff106 100644 --- a/api/apiFunctions.ts +++ b/api/apiFunctions.ts @@ -398,7 +398,6 @@ export async function createEntry({ } /** - * Update an entry * @param props - The entry data * @param props.entryId - The entry ID @@ -427,6 +426,34 @@ export async function updateEntry({ } } +/** + * Updates the name of an entry + * @param {object} params - The parameters object + * @param {string} params.entryId - The ID of the entry to update + * @param {string} params.entryName - The new name for the entry + * @returns {Models.Document | Error} - The entry object or an error + */ +export async function updateEntryName({ + entryId, + entryName, +}: { + entryId: string; + entryName: string; +}): Promise { + try { + return await databases.updateDocument( + appwriteConfig.databaseId, + Collection.ENTRIES, + entryId, + { + name: entryName, + }, + ); + } catch (error) { + throw error; + } +} + /** * Retrieves a list of all leagues. * @returns {Models.Document[]} A list of all available leagues. 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 516631f6..aebe0063 100644 --- a/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.test.tsx +++ b/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.test.tsx @@ -1,7 +1,11 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import Week from './Week'; -import { createWeeklyPicks, getCurrentUserEntries } from '@/api/apiFunctions'; +import { + createWeeklyPicks, + getCurrentUserEntries, + updateEntryName, +} from '@/api/apiFunctions'; import Alert from '@/components/AlertNotification/AlertNotification'; import { AlertVariants } from '@/components/AlertNotification/Alerts.enum'; import { toast } from 'react-hot-toast'; @@ -55,6 +59,7 @@ jest.mock('@/api/apiFunctions', () => ({ createWeeklyPicks: jest.fn(), getAllWeeklyPicks: jest.fn(), getCurrentUserEntries: jest.fn(), + updateEntryName: jest.fn(), })); jest.mock('@/utils/utils', () => { @@ -119,6 +124,100 @@ const updatedWeeklyPicks = { }, }; +describe('Entry Name Editiing', () => { + beforeEach(() => { + mockUseAuthContext.isSignedIn = true; + (getCurrentUserEntries as jest.Mock).mockResolvedValue([ + { + $id: '123', + name: 'Entry 1', + user: '123', + league: '123', + selectedTeams: [], + eliminated: false, + }, + ]); + }); + + it('should display the edit button', async () => { + render( + , + ); + + await waitFor(() => { + const editEntryNameButton = screen.getByTestId('edit-entry-name-button'); + expect(editEntryNameButton).toBeInTheDocument(); + }); + }); + + it('should switch to edit mode when the button is clicked', async () => { + render( + , + ); + + await waitFor(() => { + expect(screen.getByTestId('edit-entry-name-button')).toBeInTheDocument(); + }); + fireEvent.click(screen.getByTestId('edit-entry-name-button')); + + const entryNameInput = screen.getByTestId('entry-name-input'); + const cancelButton = screen.getByTestId('cancel-editing-button'); + const acceptButton = screen.getByTestId('save-entry-name-button'); + + expect(entryNameInput).toBeInTheDocument(); + expect(cancelButton).toBeInTheDocument(); + expect(acceptButton).toBeInTheDocument(); + }); + + it('should switch to view mode when the cancel button is clicked', async () => { + render( + , + ); + + await waitFor(() => { + expect(screen.getByTestId('edit-entry-name-button')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByTestId('edit-entry-name-button')); + fireEvent.click(screen.getByTestId('cancel-editing-button')); + + await waitFor(() => { + expect(screen.queryByTestId('entry-name-input')).not.toBeInTheDocument(); + }); + expect(screen.getByTestId('week__entry-name')).toHaveTextContent('Entry 1'); + }); + + it('should update the entry name when valid name is submitted', async () => { + (updateEntryName as jest.Mock).mockResolvedValue({ + name: 'New Entry Name', + }); + render( + , + ); + + await waitFor(() => { + const editEntryNameButton = screen.getByTestId('edit-entry-name-button'); + expect(editEntryNameButton).toBeInTheDocument(); + fireEvent.click(editEntryNameButton); + }); + + const input = screen.getByTestId('entry-name-input'); + fireEvent.change(input, { target: { value: 'New Entry Name' } }); + + fireEvent.click(screen.getByTestId('save-entry-name-button')); + + await waitFor(() => { + expect(screen.getByTestId('week__entry-name')).toHaveTextContent( + 'New Entry Name', + ); + }); + expect(updateEntryName).toHaveBeenCalledWith({ + entryId: entry, + entryName: 'New Entry Name', + }); + }); +}); + describe('League Week Picks', () => { const setUserPick = jest.fn(); const updateWeeklyPicks = jest.fn(); @@ -186,8 +285,12 @@ 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.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 6edab010..d31da025 100644 --- a/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.tsx +++ b/app/(main)/league/[leagueId]/entry/[entryId]/week/Week.tsx @@ -8,8 +8,9 @@ import { FormItem, FormControl, FormMessage, + Form, } from '@/components/Form/Form'; -import { FormProvider, Control, useForm } from 'react-hook-form'; +import { FormProvider, Control, useForm, SubmitHandler } from 'react-hook-form'; import { z } from 'zod'; import { IWeekProps } from './Week.interface'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -20,6 +21,7 @@ import { getCurrentUserEntries, getCurrentLeague, getGameWeek, + updateEntryName, } from '@/api/apiFunctions'; import { ILeague } from '@/api/apiFunctions.interface'; import WeekTeams from './WeekTeams'; @@ -33,8 +35,10 @@ import { cn, getNFLTeamLogo } from '@/utils/utils'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import LinkCustom from '@/components/LinkCustom/LinkCustom'; -import { ChevronLeft } from 'lucide-react'; +import { Check, ChevronLeft, Pen, X } from 'lucide-react'; import Heading from '@/components/Heading/Heading'; +import { Button } from '@/components/Button/Button'; +import { Input } from '@/components/Input/Input'; /** * Renders the weekly picks page. @@ -52,11 +56,39 @@ const Week = ({ entry, league, NFLTeams, week }: IWeekProps): JSX.Element => { const [loadingData, setLoadingData] = useState(true); const [loadingTeamName, setLoadingTeamName] = useState(null); const [userPick, setUserPick] = useState(''); + const [isEditing, setIsEditing] = useState(false); + const { user, updateCurrentWeek, updateWeeklyPicks, weeklyPicks } = useDataStore((state) => state); const { isSignedIn } = useAuthContext(); const router = useRouter(); + const NFLTeamsList = NFLTeams.map((team) => team.teamName) as [ + string, + ...string[] + ]; + const WeekTeamsFormSchema = z.object({ + type: z.enum(NFLTeamsList, { + required_error: 'You need to select a team.', + }), + }); + const weekTeamsForm = useForm>({ + resolver: zodResolver(WeekTeamsFormSchema), + }); + + const EntryNameFormSchema = z.object({ + name: z + .string() + .min(3, 'Entry name must contain at least 3 characters') + .max(50, 'Entry name must contain no more than 50 characters'), + }); + const entryNameForm = useForm>({ + resolver: zodResolver(EntryNameFormSchema), + defaultValues: { + name: entryName, + }, + }); + /** * Fetches the current game week. * @returns {Promise} @@ -80,17 +112,6 @@ const Week = ({ entry, league, NFLTeams, week }: IWeekProps): JSX.Element => { setSelectedLeague(res); }; - const NFLTeamsList = NFLTeams.map((team) => team.teamName) as [ - string, - ...string[] - ]; - - const FormSchema = z.object({ - type: z.enum(NFLTeamsList, { - required_error: 'You need to select a team.', - }), - }); - /** * Fetches the league's weekly pick results for the user and set the user pick. * @returns {Promise} @@ -158,8 +179,10 @@ const Week = ({ entry, league, NFLTeams, week }: IWeekProps): JSX.Element => { if (!currentEntry) { throw new Error('Entry not found'); } - + setEntryName(currentEntry.name); + entryNameForm.reset({ name: currentEntry.name }); + let entryHistory = currentEntry?.selectedTeams || []; if (currentEntry?.selectedTeams.length > 0) { @@ -176,10 +199,6 @@ const Week = ({ entry, league, NFLTeams, week }: IWeekProps): JSX.Element => { } }; - const form = useForm>({ - resolver: zodResolver(FormSchema), - }); - /** * Get selected teams for the current user entry. * @returns {Promise} The selected teams @@ -228,6 +247,25 @@ const Week = ({ entry, league, NFLTeams, week }: IWeekProps): JSX.Element => { } }; + /** + * handles the form submission + * @param data - the form data + * @returns {void} + */ + const onSubmit: SubmitHandler> = async ( + data, + ): Promise => { + const { name } = data; + try { + await updateEntryName({ entryId: entry, entryName: name }); + setEntryName(name); + entryNameForm.reset({ name: name }); + setIsEditing(false); + } catch (error) { + throw error; + } + }; + useEffect(() => { if (!selectedLeague) { getSelectedLeague(); @@ -274,18 +312,85 @@ 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`} - - {entryName} + + {`Week ${week} pick`} +
+ {isEditing ? ( + <> +
+ + } + name="name" + render={({ field }) => ( + +
+
+ + + + {entryNameForm.formState.errors.name && ( + + )} +
+ + +
+
+ )} + /> + + + + ) : ( + <> + + {entryName} + + + + )} +
{pickHistory.length > 0 && (
{
)} - +
} + control={weekTeamsForm.control as Control} name="type" render={({ field }) => (