Skip to content

Commit

Permalink
feat: add show reviews and stats components
Browse files Browse the repository at this point in the history
  • Loading branch information
sgomez committed Apr 28, 2024
1 parent 4a32f29 commit 4c8cd7a
Show file tree
Hide file tree
Showing 19 changed files with 436 additions and 68 deletions.
4 changes: 2 additions & 2 deletions e2e/books/adding-book.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ test.describe('Adding a book', () => {
)
await bookPage.submit()
// Assert
await expect(page).toHaveURL(/books\/[\dA-Z]{26}$/)
await expect(page).toHaveURL(/books\/[\dA-Z]{26}/)
await expect(page.getByLabel('Título')).toContainText('A new book')
})

Expand All @@ -30,7 +30,7 @@ test.describe('Adding a book', () => {
)
await bookPage.submit()
// Assert
await expect(page).toHaveURL(/books\/[\dA-Z]{26}$/)
await expect(page).toHaveURL(/books\/[\dA-Z]{26}/)
await expect(page.getByLabel('Autores')).toContainText(
'Jenny Doe, John Doe',
)
Expand Down
55 changes: 55 additions & 0 deletions src/app/books/[id]/(view)/components/book-view-tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use client'

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

const TABS = [
{
target: 'reviews',
title: 'Reseñas',
},
{
target: 'loans',
title: 'Préstamos',
},
] as ViewTab[]

interface ViewTab {
target: string
title: string
}

interface ViewTabsProperties {
children: ReactNode
id: string
}

export function BookViewTabs({ children, id }: ViewTabsProperties) {
const pathname = usePathname()

return (
<>
<Tabs
aria-label="Opciones"
classNames={{
tab: 'max-w-fit',
tabList: 'w-full',
}}
items={TABS}
selectedKey={pathname}
>
{({ target, title }) => (
<Tab
as={NextLink}
href={`/books/${id}/${target}`}
key={`/books/${id}/${target}`}
title={title}
/>
)}
</Tabs>
<div className="mt-4">{children}</div>
</>
)
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,28 @@
'use client'

import { Button } from '@nextui-org/button'
import {
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
} from '@nextui-org/table'
import { format } from 'date-fns'
import { es } from 'date-fns/locale'
import Link from 'next/link'

import { BookViewBreadcrumbs } from '@/app/books/[id]/(view)/components/book-view-breadcrumbs'
import { BookCard } from '@/app/books/components/book-card'
import { BookViewBreadcrumbs } from '@/app/books/components/book-view-breadcrumbs'
import { BookResponse } from '@/core/book/dto/responses/book.response'
import { HistoricalLoansResponse } from '@/core/loan/dto/responses/historical-loans.response'
import { UserResponse } from '@/core/user/dto/responses/user.response'

interface BookPageProperties {
book: BookResponse
historicalLoans: HistoricalLoansResponse[]
user?: UserResponse
}

export function BookView(properties: BookPageProperties) {
const { book, historicalLoans, user } = properties
const { book, user } = properties
return (
<>
<main className="flex flex-col gap-4 px-4 md:px-0">
<BookViewBreadcrumbs title={book.title} />
<div className="flex flex-col gap-4 md:flex-row">
<BookCard book={book} me={user} />
<BookCard book={book} key={book.id} me={user} />
<div className="flex flex-col gap-4">
<h1
aria-label="Título"
Expand All @@ -58,23 +48,6 @@ export function BookView(properties: BookPageProperties) {
<LoanBy book={book} />
</div>
</div>
<h2 className="mt-4 text-3xl font-bold">Histórico de préstamos</h2>
<Table aria-label="Listado historico" isStriped>
<TableHeader>
<TableColumn>Usuario</TableColumn>
<TableColumn>Fecha de préstamo</TableColumn>
<TableColumn>Fecha de devolución</TableColumn>
</TableHeader>
<TableBody items={historicalLoans}>
{(historicalLoan) => (
<TableRow key={historicalLoan.id}>
<TableCell>{historicalLoan.user.name}</TableCell>
<TableCell>{shortDate(historicalLoan.startsAt)}</TableCell>
<TableCell>{shortDate(historicalLoan.finishedAt)}</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</main>
</>
)
Expand All @@ -98,9 +71,3 @@ function longDate(date: string | Date) {
locale: es,
})
}

function shortDate(date: string | Date) {
return format(new Date(date), 'dd/MM/yyyy', {
locale: es,
})
}
38 changes: 38 additions & 0 deletions src/app/books/[id]/(view)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Divider } from '@nextui-org/divider'
import { notFound } from 'next/navigation'
import { ReactNode } from 'react'

import { BookView } from '@/app/books/[id]/(view)/components/book-view'
import { BookViewTabs } from '@/app/books/[id]/(view)/components/book-view-tabs'
import { findBook } from '@/core/book/infrastructure/actions/find-book'
import { UserResponse } from '@/core/user/dto/responses/user.response'
import { me } from '@/core/user/infrastructure/actions/me'

interface PageParameters {
id: string
}

export default async function Page({
children,
params,
}: {
children: ReactNode
params: PageParameters
}) {
const book = await findBook(params.id)
const user = (await me()) as UserResponse

if (!book) {
return notFound()
}

return (
<>
<div className="flex flex-col gap-4">
<BookView book={book} user={user} />
<Divider />
<BookViewTabs id={params.id}>{children}</BookViewTabs>
</div>
</>
)
}
50 changes: 50 additions & 0 deletions src/app/books/[id]/(view)/loans/components/book-view-tab-loans.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client'

import {
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
} from '@nextui-org/table'
import { format } from 'date-fns'
import { es } from 'date-fns/locale'

import { HistoricalLoansResponse } from '@/core/loan/dto/responses/historical-loans.response'

interface BookViewTabLoansProperties {
historicalLoans: HistoricalLoansResponse[]
}

export function BookViewTabLoans({
historicalLoans,
}: BookViewTabLoansProperties) {
return (
<>
<h2 className="mt-4 text-3xl font-bold">Histórico de préstamos</h2>
<Table aria-label="Listado historico" isStriped>
<TableHeader>
<TableColumn>Usuario</TableColumn>
<TableColumn>Fecha de préstamo</TableColumn>
<TableColumn>Fecha de devolución</TableColumn>
</TableHeader>
<TableBody items={historicalLoans}>
{(historicalLoan) => (
<TableRow key={historicalLoan.id}>
<TableCell>{historicalLoan.user.name}</TableCell>
<TableCell>{shortDate(historicalLoan.startsAt)}</TableCell>
<TableCell>{shortDate(historicalLoan.finishedAt)}</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</>
)
}

function shortDate(date: string | Date) {
return format(new Date(date), 'dd/MM/yyyy', {
locale: es,
})
}
12 changes: 12 additions & 0 deletions src/app/books/[id]/(view)/loans/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { BookViewTabLoans } from '@/app/books/[id]/(view)/loans/components/book-view-tab-loans'
import { getHistoricalLoans } from '@/core/loan/infrastructure/actions/get-historical-loans'

interface PageParameters {
id: string
}

export default async function Page({ params }: { params: PageParameters }) {
const historicalLoans = await getHistoricalLoans(params.id)

return <BookViewTabLoans historicalLoans={historicalLoans} />
}
13 changes: 13 additions & 0 deletions src/app/books/[id]/(view)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { redirect } from 'next/navigation'

interface PageParameters {
id: string
}

export default async function Page({
params: { id },
}: {
params: PageParameters
}) {
return redirect(`/books/${id}/reviews`)
}
75 changes: 75 additions & 0 deletions src/app/books/[id]/(view)/reviews/components/book-review-stats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use client'

import { PencilIcon, StarIcon } from '@heroicons/react/20/solid'
import { Button } from '@nextui-org/button'
import { Card, CardBody, CardFooter, CardHeader } from '@nextui-org/card'
import { Progress } from '@nextui-org/progress'
import React from 'react'

import { ReviewStatsResponse } from '@/core/review/dto/responses/review-stats.response'

interface BookReviewStatsProperties {
reviewsStats: ReviewStatsResponse[]
}

export function BookReviewStats({ reviewsStats }: BookReviewStatsProperties) {
const totalReviews = countReviews(reviewsStats)
const totalScore = sumScores(reviewsStats)
const meanScore = totalReviews > 0 ? totalScore / totalReviews : 0

const progressBars = reviewsStats.map((reviewStats) => (
<Progress
color="warning"
key={reviewStats.score}
label={`${pluralize(reviewStats.score, 'estrella', 'estrellas')}`}
maxValue={totalReviews > 0 ? totalReviews : 100}
showValueLabel
value={reviewStats.reviews}
/>
))

return (
<Card shadow="sm">
<CardHeader className="flex items-center gap-2">
<StarIcon className="h-5 w-5 fill-amber-300" />
<span className="text-large font-semibold">{meanScore.toFixed(1)}</span>
<span className="text-default-500">
• (Basada en {pluralize(totalReviews, 'reseña', 'reseñas')})
</span>
</CardHeader>
<CardBody className="flex flex-col gap-2">{progressBars}</CardBody>
<CardFooter className="flex w-full flex-col items-start gap-4">
<Button
fullWidth
radius="full"
startContent={<PencilIcon className="h-4 w-4" />}
variant="bordered"
>
Escribir una reseña
</Button>
<p className="px-2 text-small text-default-500">
Comparte tu opinión sobre este libro.
</p>
</CardFooter>
</Card>
)
}

function countReviews(reviewsStats: Readonly<ReviewStatsResponse[]>) {
return reviewsStats.reduce(
(accumulator, reviewStats) => accumulator + reviewStats.reviews,
0,
)
}

function sumScores(reviewsStats: Readonly<ReviewStatsResponse[]>): number {
return reviewsStats.reduce(
(accumulator, reviewStats) =>
accumulator + reviewStats.reviews * reviewStats.score,
0,
)
}

function pluralize(amount: number, singular: string, plural: string): string {
return `${amount} ${amount === 1 ? singular : plural}`
}
54 changes: 54 additions & 0 deletions src/app/books/[id]/(view)/reviews/components/book-review.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use client'

import { User } from '@nextui-org/user'
import { format } from 'date-fns'
import { es } from 'date-fns/locale'
import React from 'react'

import { StarIcon } from '@/components/image/star-icon'

interface BookReviewProperties {
createdAt: string
description: string
score: number
title: string
user: {
image: string
name: string
}
}

export function BookReview({
createdAt,
description,
score,
title,
user,
}: BookReviewProperties) {
return (
<>
<div className="flex flex-col">
<div className="flex items-center justify-between">
<User
avatarProps={{
src: user.image,
}}
description={format(createdAt, "dd 'de' MMMM yyyy", { locale: es })}
name={user.name}
/>
<div className="flex flex-row">
<StarIcon isActive size="md" />
<StarIcon isActive={score >= 2} />
<StarIcon isActive={score >= 3} />
<StarIcon isActive={score >= 4} />
<StarIcon isActive={score >= 5} />
</div>
</div>
<div className="mt-4 w-full">
<p className="font-medium text-default-900">{title}</p>
<p className="mt-2 text-default-500">{description}</p>
</div>
</div>
</>
)
}
Loading

0 comments on commit 4c8cd7a

Please sign in to comment.