From cc490aa6870076daa0d2ae3ecd784959bd2ab051 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 14:57:09 +0000 Subject: [PATCH 1/5] Initial plan From b3bc4bbebc829dc77a4c2c1dc95dbc9f4108eabe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 15:08:17 +0000 Subject: [PATCH 2/5] feat: add Cypress E2E tests for home, tasks, and notes management Agent-Logs-Url: https://github.com/RMCampos/tasknote/sessions/b58034df-7cc2-4f3b-aebb-6ff9b948d992 Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com> --- client/cypress/e2e/home.cy.ts | 199 +++++++++++++++++++++++ client/cypress/e2e/notes.cy.ts | 278 +++++++++++++++++++++++++++++++++ client/cypress/e2e/tasks.cy.ts | 236 ++++++++++++++++++++++++++++ 3 files changed, 713 insertions(+) create mode 100644 client/cypress/e2e/home.cy.ts create mode 100644 client/cypress/e2e/notes.cy.ts create mode 100644 client/cypress/e2e/tasks.cy.ts diff --git a/client/cypress/e2e/home.cy.ts b/client/cypress/e2e/home.cy.ts new file mode 100644 index 00000000..7c95e4ac --- /dev/null +++ b/client/cypress/e2e/home.cy.ts @@ -0,0 +1,199 @@ +/** + * E2E tests for Home page management: searching and filtering tasks and notes. + */ + +const mockUser = { + userId: 1, + name: 'Test User', + email: 'test@example.com', + admin: false, + createdAt: '2026-01-01T00:00:00.000Z', + gravatarImageUrl: '', + lang: 'en' +}; + +const mockTasks = [ + { + id: 1, + description: 'Buy groceries', + done: false, + highPriority: true, + dueDate: '2026-06-01', + dueDateFmt: 'Jun 1, 2026', + lastUpdate: '2026-05-01', + tag: 'personal', + urls: [] + }, + { + id: 2, + description: 'Write report', + done: false, + highPriority: false, + dueDate: '', + dueDateFmt: '', + lastUpdate: '2026-05-02', + tag: 'work', + urls: [] + } +]; + +const mockNotes = [ + { + id: 1, + title: 'Meeting notes', + description: 'Discuss quarterly goals', + url: null, + tag: 'work', + lastUpdate: '2026-05-01', + shared: false, + shareToken: null + }, + { + id: 2, + title: 'Recipe', + description: 'Pasta carbonara recipe', + url: null, + tag: 'personal', + lastUpdate: '2026-05-02', + shared: false, + shareToken: null + } +]; + +const mockTags = ['personal', 'work']; + +describe('Home Management', () => { + beforeEach(() => { + cy.intercept('GET', /\/rest\/user-sessions\/refresh/, { + statusCode: 200, + body: { token: 'fake-jwt-token', ...mockUser } + }).as('refreshToken'); + + cy.intercept('GET', /\/rest\/home\/tasks\/tags/, { + statusCode: 200, + body: mockTags + }).as('getTags'); + + cy.intercept('GET', /\/rest\/tasks$/, { + statusCode: 200, + body: mockTasks + }).as('getTasks'); + + cy.intercept('GET', /\/rest\/notes$/, { + statusCode: 200, + body: mockNotes + }).as('getNotes'); + + cy.visit('/home', { + onBeforeLoad(win) { + win.localStorage.setItem('TASKNOTE-TOKEN', 'fake-jwt-token'); + win.localStorage.setItem('TASKNOTE-USER', JSON.stringify(mockUser)); + } + }); + }); + + /** + * Home page display + */ + describe('Display', () => { + it('shows tasks and notes after loading', () => { + cy.wait('@getTasks'); + cy.wait('@getNotes'); + + cy.contains('Buy groceries').should('be.visible'); + cy.contains('Write report').should('be.visible'); + cy.contains('Meeting notes').should('be.visible'); + cy.contains('Recipe').should('be.visible'); + }); + }); + + /** + * Search / text filtering + */ + describe('Search', () => { + beforeEach(() => { + cy.wait('@getTasks'); + cy.wait('@getNotes'); + }); + + it('filters items by search text matching a task description', () => { + cy.get('input[placeholder="Filter tasks & notes"]').type('groceries'); + + cy.contains('Buy groceries').should('be.visible'); + cy.contains('Write report').should('not.exist'); + cy.contains('Meeting notes').should('not.exist'); + cy.contains('Recipe').should('not.exist'); + }); + + it('filters items by search text matching a note title', () => { + cy.get('input[placeholder="Filter tasks & notes"]').type('Meeting'); + + cy.contains('Meeting notes').should('be.visible'); + cy.contains('Buy groceries').should('not.exist'); + cy.contains('Recipe').should('not.exist'); + }); + + it('restores all items after clearing the search', () => { + cy.get('input[placeholder="Filter tasks & notes"]').type('groceries'); + cy.contains('Write report').should('not.exist'); + + cy.get('input[placeholder="Filter tasks & notes"]').clear(); + + cy.contains('Buy groceries').should('be.visible'); + cy.contains('Meeting notes').should('be.visible'); + }); + }); + + /** + * Dropdown filter + */ + describe('Dropdown Filter', () => { + beforeEach(() => { + cy.wait('@getTasks'); + cy.wait('@getNotes'); + }); + + it('shows only tasks when Tasks filter is selected', () => { + cy.get('[data-testid="dropdown-tag-filter"]').click(); + cy.contains('Tasks').click(); + + cy.contains('Buy groceries').should('be.visible'); + cy.contains('Write report').should('be.visible'); + cy.contains('Meeting notes').should('not.exist'); + cy.contains('Recipe').should('not.exist'); + }); + + it('shows only notes when Notes filter is selected', () => { + cy.get('[data-testid="dropdown-tag-filter"]').click(); + cy.contains('Notes').click(); + + cy.contains('Meeting notes').should('be.visible'); + cy.contains('Recipe').should('be.visible'); + cy.contains('Buy groceries').should('not.exist'); + cy.contains('Write report').should('not.exist'); + }); + + it('shows everything when Everything filter is selected', () => { + cy.get('[data-testid="dropdown-tag-filter"]').click(); + cy.contains('Tasks').click(); + + cy.get('[data-testid="dropdown-tag-filter"]').click(); + cy.contains('Everything').click(); + + cy.contains('Buy groceries').should('be.visible'); + cy.contains('Meeting notes').should('be.visible'); + }); + + it('filters by tag when a tag is selected', () => { + cy.wait('@getTags'); + + cy.get('[data-testid="dropdown-tag-filter"]').click(); + cy.contains('#personal').click(); + + cy.contains('Buy groceries').should('be.visible'); + cy.contains('Recipe').should('be.visible'); + cy.contains('Write report').should('not.exist'); + cy.contains('Meeting notes').should('not.exist'); + }); + }); +}); diff --git a/client/cypress/e2e/notes.cy.ts b/client/cypress/e2e/notes.cy.ts new file mode 100644 index 00000000..8eb35ff8 --- /dev/null +++ b/client/cypress/e2e/notes.cy.ts @@ -0,0 +1,278 @@ +/** + * E2E tests for Notes Management: Create, Update, Delete, and Share notes. + */ + +const mockUser = { + userId: 1, + name: 'Test User', + email: 'test@example.com', + admin: false, + createdAt: '2026-01-01T00:00:00.000Z', + gravatarImageUrl: '', + lang: 'en' +}; + +const mockNote = { + id: 1, + title: 'Meeting notes', + description: 'Discuss quarterly goals', + url: null, + tag: 'work', + lastUpdate: '2026-05-01', + shared: false, + shareToken: null +}; + +describe('Notes Management', () => { + /** + * Sets up authentication intercepts and visits the given path. + * + * @param {string} path - The path to visit. + */ + const visitWithAuth = (path: string): void => { + cy.intercept('GET', /\/rest\/user-sessions\/refresh/, { + statusCode: 200, + body: { token: 'fake-jwt-token', ...mockUser } + }).as('refreshToken'); + + cy.intercept('GET', /\/rest\/home\/tasks\/tags/, { + statusCode: 200, + body: ['work', 'personal'] + }).as('getTags'); + + cy.visit(path, { + onBeforeLoad(win) { + win.localStorage.setItem('TASKNOTE-TOKEN', 'fake-jwt-token'); + win.localStorage.setItem('TASKNOTE-USER', JSON.stringify(mockUser)); + } + }); + }; + + /** + * Create note + */ + describe('Create', () => { + beforeEach(() => { + visitWithAuth('/notes/new'); + }); + + it('displays the add note form', () => { + cy.contains('Add Note').should('be.visible'); + cy.get('input[name="note_title"]').should('be.visible'); + cy.get('textarea[name="note_description"]').should('be.visible'); + cy.contains('button', 'Save note').should('be.visible'); + }); + + it('shows a validation error when submitted without a title', () => { + cy.contains('button', 'Save note').click(); + + cy.get('.alert-danger').should('be.visible'); + }); + + it('shows a validation error when submitted without content', () => { + cy.get('input[name="note_title"]').type('A note title'); + cy.contains('button', 'Save note').click(); + + cy.get('.alert-danger').should('be.visible'); + }); + + it('creates a note and navigates to home on success', () => { + cy.intercept('POST', /\/rest\/notes/, { + statusCode: 201, + body: { + id: 10, + title: 'New note', + description: 'Some content', + url: null, + tag: '', + lastUpdate: '', + shared: false, + shareToken: null + } + }).as('createNote'); + + cy.intercept('GET', /\/rest\/tasks$/, { statusCode: 200, body: [] }).as('getTasks'); + cy.intercept('GET', /\/rest\/notes$/, { statusCode: 200, body: [] }).as('getNotes'); + + cy.get('input[name="note_title"]').type('New note'); + cy.get('textarea[name="note_description"]').type('Some content'); + cy.contains('button', 'Save note').click(); + + cy.wait('@createNote'); + cy.url().should('include', '/home'); + }); + + it('shows an error alert when the API returns an error on create', () => { + cy.intercept('POST', /\/rest\/notes/, { + statusCode: 500, + body: { message: 'Internal Server Error' } + }).as('createNoteFail'); + + cy.get('input[name="note_title"]').type('Failing note'); + cy.get('textarea[name="note_description"]').type('Some content'); + cy.contains('button', 'Save note').click(); + + cy.wait('@createNoteFail'); + cy.get('.alert-danger').should('be.visible'); + }); + + it('navigates back to home when Cancel is clicked', () => { + cy.intercept('GET', /\/rest\/tasks$/, { statusCode: 200, body: [] }).as('getTasks'); + cy.intercept('GET', /\/rest\/notes$/, { statusCode: 200, body: [] }).as('getNotes'); + + cy.contains('button', 'Cancel').click(); + + cy.url().should('include', '/home'); + }); + }); + + /** + * Update note + */ + describe('Update', () => { + beforeEach(() => { + cy.intercept('GET', /\/rest\/notes\/\d+/, { + statusCode: 200, + body: mockNote + }).as('getNote'); + + visitWithAuth('/notes/edit/1'); + cy.wait('@getNote'); + }); + + it('pre-fills the form with the existing note data', () => { + cy.get('input[name="note_title"]').should('have.value', 'Meeting notes'); + cy.get('textarea[name="note_description"]').should('have.value', 'Discuss quarterly goals'); + }); + + it('updates the note and navigates to home on success', () => { + cy.intercept('PATCH', /\/rest\/notes\/\d+/, { + statusCode: 200, + body: { ...mockNote, title: 'Updated meeting notes' } + }).as('updateNote'); + + cy.intercept('GET', /\/rest\/tasks$/, { statusCode: 200, body: [] }).as('getTasks'); + cy.intercept('GET', /\/rest\/notes$/, { statusCode: 200, body: [] }).as('getNotes'); + + cy.get('input[name="note_title"]').clear().type('Updated meeting notes'); + cy.contains('button', 'Save note').click(); + + cy.wait('@updateNote'); + cy.url().should('include', '/home'); + }); + + it('shows an error alert when the update API call fails', () => { + cy.intercept('PATCH', /\/rest\/notes\/\d+/, { + statusCode: 500, + body: { message: 'Internal Server Error' } + }).as('updateNoteFail'); + + cy.get('input[name="note_title"]').clear().type('Updated note'); + cy.contains('button', 'Save note').click(); + + cy.wait('@updateNoteFail'); + cy.get('.alert-danger').should('be.visible'); + }); + }); + + /** + * Delete note + */ + describe('Delete', () => { + beforeEach(() => { + cy.intercept('GET', /\/rest\/tasks$/, { + statusCode: 200, + body: [] + }).as('getTasks'); + + cy.intercept('GET', /\/rest\/notes$/, { + statusCode: 200, + body: [mockNote] + }).as('getNotes'); + + visitWithAuth('/home'); + cy.wait('@getNotes'); + }); + + it('deletes a note via the note dropdown menu', () => { + cy.intercept('DELETE', /\/rest\/notes\/\d+/, { + statusCode: 204 + }).as('deleteNote'); + + cy.intercept('GET', /\/rest\/notes$/, { + statusCode: 200, + body: [] + }).as('getNotesAfterDelete'); + + cy.get('[data-testid="note-dropdown-menu-1"]').click(); + cy.get('[data-testid="note-dropdown-delete-item-1"]').click(); + + cy.wait('@deleteNote'); + }); + }); + + /** + * Share note + */ + describe('Share', () => { + beforeEach(() => { + cy.intercept('GET', /\/rest\/tasks$/, { + statusCode: 200, + body: [] + }).as('getTasks'); + + cy.intercept('GET', /\/rest\/notes$/, { + statusCode: 200, + body: [mockNote] + }).as('getNotes'); + + visitWithAuth('/home'); + cy.wait('@getNotes'); + }); + + it('shares a note via the note dropdown menu', () => { + cy.intercept('PUT', /\/rest\/notes\/\d+\/share/, { + statusCode: 200, + body: { ...mockNote, shared: true, shareToken: 'abc123' } + }).as('shareNote'); + + cy.intercept('GET', /\/rest\/notes$/, { + statusCode: 200, + body: [{ ...mockNote, shared: true, shareToken: 'abc123' }] + }).as('getNotesAfterShare'); + + cy.get('[data-testid="note-dropdown-menu-1"]').click(); + cy.get('[data-testid="note-dropdown-share-item-1"]').click(); + + cy.wait('@shareNote'); + }); + + it('unshares a shared note via the note dropdown menu', () => { + const sharedNote = { ...mockNote, shared: true, shareToken: 'abc123' }; + + cy.intercept('GET', /\/rest\/notes$/, { + statusCode: 200, + body: [sharedNote] + }).as('getSharedNotes'); + + cy.intercept('PUT', /\/rest\/notes\/\d+\/unshare/, { + statusCode: 200, + body: { ...sharedNote, shared: false, shareToken: null } + }).as('unshareNote'); + + cy.visit('/home', { + onBeforeLoad(win) { + win.localStorage.setItem('TASKNOTE-TOKEN', 'fake-jwt-token'); + win.localStorage.setItem('TASKNOTE-USER', JSON.stringify(mockUser)); + } + }); + + cy.wait('@getSharedNotes'); + + cy.get('[data-testid="note-dropdown-menu-1"]').click(); + cy.get('[data-testid="note-dropdown-share-item-1"]').click(); + + cy.wait('@unshareNote'); + }); + }); +}); diff --git a/client/cypress/e2e/tasks.cy.ts b/client/cypress/e2e/tasks.cy.ts new file mode 100644 index 00000000..c1085376 --- /dev/null +++ b/client/cypress/e2e/tasks.cy.ts @@ -0,0 +1,236 @@ +/** + * E2E tests for Task Management: Create, Update, and Delete tasks. + */ + +const mockUser = { + userId: 1, + name: 'Test User', + email: 'test@example.com', + admin: false, + createdAt: '2026-01-01T00:00:00.000Z', + gravatarImageUrl: '', + lang: 'en' +}; + +const mockTask = { + id: 1, + description: 'Buy groceries', + done: false, + highPriority: false, + dueDate: '', + dueDateFmt: '', + lastUpdate: '2026-05-01', + tag: 'personal', + urls: [] +}; + +describe('Task Management', () => { + /** + * Sets up authentication intercepts and visits the given path. + * + * @param {string} path - The path to visit. + */ + const visitWithAuth = (path: string): void => { + cy.intercept('GET', /\/rest\/user-sessions\/refresh/, { + statusCode: 200, + body: { token: 'fake-jwt-token', ...mockUser } + }).as('refreshToken'); + + cy.intercept('GET', /\/rest\/home\/tasks\/tags/, { + statusCode: 200, + body: ['personal', 'work'] + }).as('getTags'); + + cy.visit(path, { + onBeforeLoad(win) { + win.localStorage.setItem('TASKNOTE-TOKEN', 'fake-jwt-token'); + win.localStorage.setItem('TASKNOTE-USER', JSON.stringify(mockUser)); + } + }); + }; + + /** + * Create task + */ + describe('Create', () => { + beforeEach(() => { + visitWithAuth('/tasks/new'); + }); + + it('displays the add task form', () => { + cy.contains('Add Task').should('be.visible'); + cy.get('input[name="description"]').should('be.visible'); + cy.get('input[name="url"]').should('be.visible'); + cy.get('input[name="tag"]').should('be.visible'); + cy.contains('button', 'Save task').should('be.visible'); + }); + + it('shows a validation error when submitted without a description', () => { + cy.contains('button', 'Save task').click(); + + cy.get('.alert-danger').should('be.visible'); + }); + + it('creates a task and navigates to home on success', () => { + cy.intercept('POST', /\/rest\/tasks/, { + statusCode: 201, + body: { + id: 10, + description: 'New task', + done: false, + highPriority: false, + dueDate: '', + dueDateFmt: '', + lastUpdate: '', + tag: '', + urls: [] + } + }).as('createTask'); + + cy.intercept('GET', /\/rest\/tasks$/, { statusCode: 200, body: [] }).as('getTasks'); + cy.intercept('GET', /\/rest\/notes$/, { statusCode: 200, body: [] }).as('getNotes'); + + cy.get('input[name="description"]').type('New task'); + cy.contains('button', 'Save task').click(); + + cy.wait('@createTask'); + cy.url().should('include', '/home'); + }); + + it('shows an error alert when the API returns an error on create', () => { + cy.intercept('POST', /\/rest\/tasks/, { + statusCode: 500, + body: { message: 'Internal Server Error' } + }).as('createTaskFail'); + + cy.get('input[name="description"]').type('Failing task'); + cy.contains('button', 'Save task').click(); + + cy.wait('@createTaskFail'); + cy.get('.alert-danger').should('be.visible'); + }); + + it('navigates back to home when Cancel is clicked', () => { + cy.intercept('GET', /\/rest\/tasks$/, { statusCode: 200, body: [] }).as('getTasks'); + cy.intercept('GET', /\/rest\/notes$/, { statusCode: 200, body: [] }).as('getNotes'); + + cy.contains('button', 'Cancel').click(); + + cy.url().should('include', '/home'); + }); + }); + + /** + * Update task + */ + describe('Update', () => { + beforeEach(() => { + cy.intercept('GET', /\/rest\/tasks\/\d+/, { + statusCode: 200, + body: mockTask + }).as('getTask'); + + visitWithAuth('/tasks/edit/1'); + cy.wait('@getTask'); + }); + + it('pre-fills the form with the existing task data', () => { + cy.get('input[name="description"]').should('have.value', 'Buy groceries'); + }); + + it('updates the task and navigates to home on success', () => { + cy.intercept('PATCH', /\/rest\/tasks\/\d+/, { + statusCode: 200, + body: { ...mockTask, description: 'Buy more groceries' } + }).as('updateTask'); + + cy.intercept('GET', /\/rest\/tasks$/, { statusCode: 200, body: [] }).as('getTasks'); + cy.intercept('GET', /\/rest\/notes$/, { statusCode: 200, body: [] }).as('getNotes'); + + cy.get('input[name="description"]').clear().type('Buy more groceries'); + cy.contains('button', 'Save task').click(); + + cy.wait('@updateTask'); + cy.url().should('include', '/home'); + }); + + it('shows an error alert when the update API call fails', () => { + cy.intercept('PATCH', /\/rest\/tasks\/\d+/, { + statusCode: 500, + body: { message: 'Internal Server Error' } + }).as('updateTaskFail'); + + cy.get('input[name="description"]').clear().type('Updated task'); + cy.contains('button', 'Save task').click(); + + cy.wait('@updateTaskFail'); + cy.get('.alert-danger').should('be.visible'); + }); + }); + + /** + * Mark as done / undone + */ + describe('Mark as Done', () => { + beforeEach(() => { + cy.intercept('GET', /\/rest\/tasks$/, { + statusCode: 200, + body: [mockTask] + }).as('getTasks'); + + cy.intercept('GET', /\/rest\/notes$/, { + statusCode: 200, + body: [] + }).as('getNotes'); + + visitWithAuth('/home'); + cy.wait('@getTasks'); + }); + + it('marks a task as done via the task dropdown menu', () => { + cy.intercept('DELETE', /\/rest\/tasks\/\d+/, { + statusCode: 204 + }).as('markDone'); + + cy.intercept('GET', /\/rest\/tasks$/, { + statusCode: 200, + body: [{ ...mockTask, done: true }] + }).as('getTasksReloaded'); + + cy.contains('.task-card', 'Buy groceries').find('.dropdown-toggle').click(); + cy.contains('Done').click(); + + cy.wait('@markDone'); + }); + + it('marks a done task as undone via the task dropdown menu', () => { + cy.intercept('GET', /\/rest\/tasks$/, { + statusCode: 200, + body: [{ ...mockTask, done: true }] + }).as('getDoneTasks'); + + cy.intercept('DELETE', /\/rest\/tasks\/\d+/, { + statusCode: 204 + }).as('markUndone'); + + cy.intercept('GET', /\/rest\/tasks$/, { + statusCode: 200, + body: [{ ...mockTask, done: false }] + }).as('getTasksReloaded'); + + cy.visit('/home', { + onBeforeLoad(win) { + win.localStorage.setItem('TASKNOTE-TOKEN', 'fake-jwt-token'); + win.localStorage.setItem('TASKNOTE-USER', JSON.stringify(mockUser)); + } + }); + + cy.wait('@getDoneTasks'); + + cy.contains('.task-card', 'Buy groceries').find('.dropdown-toggle').click(); + cy.contains('Undone').click(); + + cy.wait('@markUndone'); + }); + }); +}); From 0601a81e1af14530e4b5184615a7314bffb25c8a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 16:28:03 +0000 Subject: [PATCH 3/5] fix: add data-testid to task dropdown and update Mark as Done tests Agent-Logs-Url: https://github.com/RMCampos/tasknote/sessions/05bf3f86-5dda-4c1a-9f2b-dc1f0e38c9f3 Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com> --- client/cypress/e2e/tasks.cy.ts | 8 ++++---- client/src/views/Home/index.tsx | 8 ++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/client/cypress/e2e/tasks.cy.ts b/client/cypress/e2e/tasks.cy.ts index c1085376..df1f24b7 100644 --- a/client/cypress/e2e/tasks.cy.ts +++ b/client/cypress/e2e/tasks.cy.ts @@ -197,8 +197,8 @@ describe('Task Management', () => { body: [{ ...mockTask, done: true }] }).as('getTasksReloaded'); - cy.contains('.task-card', 'Buy groceries').find('.dropdown-toggle').click(); - cy.contains('Done').click(); + cy.get('[data-testid="task-dropdown-menu-1"]').click(); + cy.get('[data-testid="task-dropdown-done-item-1"]').click(); cy.wait('@markDone'); }); @@ -227,8 +227,8 @@ describe('Task Management', () => { cy.wait('@getDoneTasks'); - cy.contains('.task-card', 'Buy groceries').find('.dropdown-toggle').click(); - cy.contains('Undone').click(); + cy.get('[data-testid="task-dropdown-menu-1"]').click(); + cy.get('[data-testid="task-dropdown-done-item-1"]').click(); cy.wait('@markUndone'); }); diff --git a/client/src/views/Home/index.tsx b/client/src/views/Home/index.tsx index 4c7c260a..cb7c2c20 100644 --- a/client/src/views/Home/index.tsx +++ b/client/src/views/Home/index.tsx @@ -487,7 +487,7 @@ function Home(): React.ReactNode {