Skip to content

Commit

Permalink
feat: add new settings page
Browse files Browse the repository at this point in the history
  • Loading branch information
sgomez committed Jan 14, 2024
1 parent 7173930 commit 323bafa
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 36 deletions.
6 changes: 3 additions & 3 deletions e2e/settings/updating-settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ test.describe('update user settings', () => {
// Arrange
await settingsPage.goto()
// Act
await settingsPage.editName('John Doe')
await settingsPage.submit()
await settingsPage.edit('name', 'John Doe')
await settingsPage.submit('name')
await page.reload()

// Assert
await expect(page).toHaveURL('/settings/profile')
await expect(page.getByPlaceholder('Nombre')).toHaveValue('John Doe')
await expect(page.getByLabel('name')).toHaveValue('John Doe')
})
})
12 changes: 8 additions & 4 deletions e2e/tests/pages/settings.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ class SettingsPage {
await this.page.goto('/settings/profile')
}

async editName(name: string) {
await this.page.getByPlaceholder('Nombre').fill(name)
async edit(label: string, name: string) {
await this.page.getByLabel(label).fill(name)
}

async submit() {
await this.page.getByRole('button', { name: 'Enviar' }).click()
async submit(label: string) {
await this.page
.getByRole('region')
.filter({ has: this.page.getByLabel(label) })
.getByRole('button', { name: 'Guardar' })
.click()
}

async restore() {
Expand Down
38 changes: 12 additions & 26 deletions src/app/settings/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,25 @@
'use client'

import { Card, CardBody, CardHeader, Tab, Tabs } from '@nextui-org/react'
import NextLink from 'next/link'
import { usePathname } from 'next/navigation'
import { ReactNode } from 'react'

import SettingsTemplate from '@/components/settings-template/settings-template'

const TABS = [
{
target: 'profile',
title: 'Perfil',
},
]

export default function Layout({ children }: { children: ReactNode }) {
const pathname = usePathname()

return (
<>
<Card className="flex flex-col gap-2 w-full p-1">
<CardHeader>
<div className="h1 font-bold text-2xl">Ajustes de usuario</div>
</CardHeader>
<CardBody className="overflow-hidden">
<Tabs
size="lg"
aria-label="Options"
selectedKey={pathname}
classNames={{
tab: 'max-w-fit',
tabList: 'w-full',
}}
>
<Tab
key="/settings/profile"
href="/settings/profile"
title="Perfil"
as={NextLink}
/>
</Tabs>
<div className="mt-4">{children}</div>
</CardBody>
</Card>
<SettingsTemplate pathname={pathname} tabs={TABS}>
{children}
</SettingsTemplate>
</>
)
}
29 changes: 27 additions & 2 deletions src/app/settings/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { redirect } from 'next/navigation'

import EditProfileForm from '@/components/edit-profile-form/edit-profile-form'
import SettingsForm from '@/components/settings-form/settings-form'
import SettingsFormInputText from '@/components/settings-form/settings-form-input-text'
import UserResponse from '@/core/user/dto/responses/user.response'
import { findUser } from '@/core/user/infrastructure/actions/find-user'
import { auth } from '@/lib/auth/auth'
Expand All @@ -16,7 +17,31 @@ export default async function Page() {

return (
<>
<EditProfileForm user={user} />
<div className="flex flex-col gap-y-4">
<SettingsForm title="Nombre visible">
<div>Introduce tu nombre completo o con el que más se te conoce.</div>
<SettingsFormInputText
field="name"
inputProperties={{
isRequired: true,
}}
status="Máximo 64 caracteres."
user={user}
/>
</SettingsForm>

<SettingsForm title="Dirección de correo electrónico">
<div>Esta es la dirección de acceso a la plataforma.</div>
<SettingsFormInputText
field="email"
inputProperties={{
isDisabled: true,
}}
status="Este campo no se puede modificar."
user={user}
/>
</SettingsForm>
</div>
</>
)
}
46 changes: 46 additions & 0 deletions src/core/user/application/update-setting.use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { err, ok, Result } from 'neverthrow'

import NotFoundError from '@/core/common/domain/errors/application/not-found-error'
import ApplicationError from '@/core/common/domain/errors/application-error'
import DomainError from '@/core/common/domain/errors/domain-error'
import Email from '@/core/common/domain/value-objects/email'
import FullName from '@/core/common/domain/value-objects/fullname'
import User from '@/core/user/domain/model/user.entity'
import Users from '@/core/user/domain/services/users.repository'
import UpdateSettingRequest from '@/core/user/dto/requests/update-setting.request'

export default class UpdateSettingUseCase {
constructor(private readonly users: Users) {}

async with(
command: UpdateSettingRequest,
): Promise<Result<void, NotFoundError | DomainError>> {
return Email.create(command.email)
.asyncAndThen((email) => this.users.findByEmail(email))
.andThen((user) => this.updateUser(user, command))
.andThen((user) => this.users.save(user))
}

private updateUser(user: User, command: UpdateSettingRequest) {
switch (command.field) {
case 'name': {
return this.updateFullName(command, user)
}
default: {
return err(
new ApplicationError(
`Field ${command.field} does not exists in User entity`,
),
)
}
}
}

private updateFullName(command: UpdateSettingRequest, user: User) {
return FullName.create(command.value).andThen((fullName) => {
user.name = fullName

return ok(user)
})
}
}
13 changes: 13 additions & 0 deletions src/core/user/dto/requests/update-setting.request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DeepReadonly } from 'ts-essentials'

type UpdateSettingRequest = DeepReadonly<{
email: string
field: string
value: string
}>

const UpdateSettingRequest = {
with: (properties: UpdateSettingRequest): UpdateSettingRequest => properties,
}

export default UpdateSettingRequest
75 changes: 75 additions & 0 deletions src/core/user/infrastructure/actions/update-setting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use server'

import { revalidateTag } from 'next/cache'
import { z } from 'zod'

import UpdateSettingRequest from '@/core/user/dto/requests/update-setting.request'
import me from '@/core/user/infrastructure/actions/me'
import container from '@/lib/container'
import FormResponse from '@/lib/zod/form-response'

export type UpdatableFields = 'name' | 'email'

interface UpdateSettingForm {
field: UpdatableFields
value: string
}

const UpdateSettingMap = {
name: z.object({
field: z.string(),
value: z.string().min(1).max(64),
}),
}

type UpdateSettingMapKeys = keyof typeof UpdateSettingMap

export async function updateSetting(
previousState: FormResponse<UpdateSettingForm>,
formData: FormData,
): Promise<FormResponse<UpdateSettingForm>> {
const user = await me()
const { field } = previousState.data

if (!user) {
return FormResponse.custom(
[field],
'Error en la sesión del usuario',
previousState.data,
)
}

const { email } = user

if (!(field in UpdateSettingMap)) {
return FormResponse.custom<UpdateSettingForm>(
[field],
'El campo no es válido',
previousState.data,
)
}

const result = UpdateSettingMap[field as UpdateSettingMapKeys].safeParse(
Object.fromEntries(formData),
)

if (!result.success) {
return FormResponse.withError<UpdateSettingForm>(
result.error,
previousState.data,
)
}

const { value } = result.data

await container.updateSetting.with(
UpdateSettingRequest.with({ email, field, value }),
)

revalidateTag(`role-for-${email}`)

return FormResponse.success<UpdateSettingForm>(
result.data as UpdateSettingForm,
'Perfil actualizado.',
)
}
4 changes: 3 additions & 1 deletion src/lib/container/container.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import CreateBookUseCase from '@/core/book/application/create-book.use-case'
import EditBookUseCase from '@/core/book/application/edit-book.use-case'
import { LoanBookUseCase } from '@/core/book/application/loan-book-use.case'
import LoanBookUseCase from '@/core/book/application/loan-book-use.case'
import ReturnBookUseCase from '@/core/book/application/return-book.use-case'
import FindAllBooksQuery from '@/core/book/infrastructure/queries/find-all-books.query'
import FindBookQuery from '@/core/book/infrastructure/queries/find-book.query'
Expand All @@ -11,6 +11,7 @@ import GetHistoricalLoansQuery from '@/core/loan/infrastructure/queries/get-hist
import LoansPrisma from '@/core/loan/infrastructure/services/loans-prisma.repository'
import EnableUserUseCase from '@/core/user/application/enable-user.use-case'
import FindUserUseCase from '@/core/user/application/find-user.use-case'
import UpdateSettingUseCase from '@/core/user/application/update-setting.use-case'
import UpdateUserUseCase from '@/core/user/application/update-user.use-case'
import FindAllUsersQuery from '@/core/user/infrastructure/queries/find-all-users.query'
import UsersPrisma from '@/core/user/infrastructure/services/users-prisma.repository'
Expand All @@ -35,6 +36,7 @@ const Container = {
getHistoricalLoans: new GetHistoricalLoansQuery(prisma),
loanBook: new LoanBookUseCase(books, loanBookService),
returnBook: new ReturnBookUseCase(books, returnBookService),
updateSetting: new UpdateSettingUseCase(users),
updateUser: new UpdateUserUseCase(users),
}
},
Expand Down

0 comments on commit 323bafa

Please sign in to comment.