Skip to content

Commit

Permalink
feat: add loan actions
Browse files Browse the repository at this point in the history
  • Loading branch information
sgomez committed Dec 8, 2023
1 parent 96c990c commit 5af8ac9
Show file tree
Hide file tree
Showing 23 changed files with 405 additions and 86 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
next.config.js
1 change: 1 addition & 0 deletions .ncurc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"upgrade": true,
"reject": [
"vitest"
]
}
13 changes: 12 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ const nextConfig = {
allowedOrigins: ['localhost:3000', 'codex.aulasoftwarelibre.uco.es'],
},
},
output: 'standalone',
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'm.media-amazon.com',
},
{
protocol: 'https',
hostname: 'gravatar.com',
},
],
},
}

module.exports = nextConfig
Binary file added public/images/logos-pie-white.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/logos-pie.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions src/app/books/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Image from 'next/image'
import { notFound } from 'next/navigation'

import BookBreadcrumbs from '@/components/book-breadcrubs/book-breadcrubs'
import BookResponse from '@/core/book/dto/responses/book.response'
import { findBook } from '@/core/book/infrastructure/actions/find-book'

interface PageParameters {
id: string
}

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

if (!book) {
return notFound()
}

return (
<main className="flex flex-col gap-4">
<BookBreadcrumbs title={book.title} />
<div className="flex gap-4">
<Image
className="h-[297px] w-[320px] object-scale-down"
alt={book.title}
width={297}
height={387}
src={book.image}
/>
<div className="flex flex-col">
<div className="font-bold text-4xl text-default-800">
{book.title}
</div>
<div className="line-clamp-1 text-xl flex-grow">
{book.authors.join(', ')}
</div>
<LoanBy book={book} />
</div>
</div>
</main>
)
}

function LoanBy({ book }: { book: BookResponse }) {
if (!book.loan) {
return null
}

return <div>Este libro se encuentra prestado a {book.loan.user.name}</div>
}
10 changes: 8 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import './globals.css'
import type { Metadata } from 'next'
import { Nunito } from 'next/font/google'

import Footer from '@/components/footer/footer'
import Header from '@/components/header'
import { Providers } from '@/components/providers'
import { findUser } from '@/core/user/infrastructure/actions/find-user'
Expand All @@ -26,11 +27,16 @@ export default async function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${inter.className} antialiased text-slate-500 dark:text-slate-400 bg-white dark:bg-slate-900 min-h-screen`}
className={`${inter.className} antialiased text-slate-500 dark:text-slate-400 bg-white dark:bg-slate-900`}
>
<Providers>
<Header user={user} />
<div className="container mx-auto pt-4 px-4">{children}</div>
<div className=" flex flex-col min-h-[calc(100vh-155px)]">
<div className="container mx-auto flex-grow pt-5 pb-5">
{children}
</div>
</div>
<Footer />
</Providers>
</body>
</html>
Expand Down
5 changes: 4 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import BookGrid from '@/components/book-grid'
import { findBooks } from '@/core/book/infrastructure/actions/find-books'
import me from '@/core/user/infrastructure/actions/me'

export default async function Home() {
const books = await findBooks()
const user = await me()

return (
<main>
<BookGrid books={books} />
<BookGrid books={books} me={user} />
</main>
)
}
24 changes: 24 additions & 0 deletions src/components/book-breadcrubs/book-breadcrubs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client'

import { BreadcrumbItem, Breadcrumbs } from '@nextui-org/react'
import { useRouter } from 'next/navigation'

interface BookBreadcrumbsProperties {
title: string
}

export default function BookBreadcrumbs(properties: BookBreadcrumbsProperties) {
const router = useRouter()
router.prefetch('/')

const { title } = properties

return (
<>
<Breadcrumbs>
<BreadcrumbItem onClick={() => router.push('/')}>Inicio</BreadcrumbItem>
<BreadcrumbItem>{title}</BreadcrumbItem>
</Breadcrumbs>
</>
)
}
91 changes: 91 additions & 0 deletions src/components/book-card/book-card-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use client'

import { BookmarkIcon, BookmarkSlashIcon } from '@heroicons/react/24/solid'
import { useEffect } from 'react'
import { useFormState } from 'react-dom'

import SubmitButton from '@/components/submit-button'
import { showToast } from '@/components/toast'
import BookResponse from '@/core/book/dto/responses/book.response'
import { loanBook } from '@/core/loan/infrastructure/actions/loan-book'
import { returnBook } from '@/core/loan/infrastructure/actions/return-book'
import UserResponse from '@/core/user/dto/responses/user.response'
import FormResponse from '@/lib/zod/form-response'

interface BookCardFormProperties {
book: BookResponse
me?: UserResponse
}

function useController(properties: BookCardFormProperties) {
const { book, me } = properties

const isLoaned = !!book.loan
const isLogged = !!me
const isOwned = isLoaned && isLogged && book.loan.user.id === me.id
const isActive = isLogged && (!isLoaned || isOwned)

const currentAction = useFormState(
isOwned ? returnBook : loanBook,
FormResponse.initialState({ bookId: book.id }),
)

return { currentAction, isActive, isOwned, ...properties }
}

export default function BookCardForm(properties: BookCardFormProperties) {
const {
currentAction: [state, action],
isActive,
isOwned,
} = useController(properties)

useEffect(() => {
if (state?.success) {
showToast(state.message)
}
}, [state])

if (!isActive) {
return null
}

return (
<>
<form action={action}>
<input type="hidden" name="bookId" value={state.data.bookId} />
{isOwned ? <ReturnButton /> : <LoanButton />}
</form>
</>
)
}

function LoanButton() {
return (
<>
<SubmitButton
isIconOnly
className="opacity-0 group-hover:opacity-100 transition group-hover:duration-300 group-hover:-translate-y-2 text-center w-14 h-14 hover:scale-105 bg-gradient-to-tr from-pink-500 to-yellow-500 shadow-2xl absolute bottom-4 right-5 z-10"
radius="full"
variant="flat"
>
<BookmarkIcon className="h-8 w-8 m-auto fill-white" />
</SubmitButton>
</>
)
}

function ReturnButton() {
return (
<>
<SubmitButton
isIconOnly
className="opacity-0 group-hover:opacity-100 transition group-hover:duration-300 group-hover:-translate-y-2 text-center w-14 h-14 hover:scale-105 bg-gradient-to-tr from-pink-500 to-yellow-500 shadow-2xl absolute bottom-4 left-5 z-10"
radius="full"
variant="flat"
>
<BookmarkSlashIcon className="h-8 w-8 m-auto fill-white" />
</SubmitButton>
</>
)
}
82 changes: 82 additions & 0 deletions src/components/book-card/book-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Card, CardBody } from '@nextui-org/react'
import Image from 'next/image'
import Link from 'next/link'

import BookCardForm from '@/components/book-card/book-card-form'
import BookResponse from '@/core/book/dto/responses/book.response'
import UserResponse from '@/core/user/dto/responses/user.response'

interface BookCardProperties {
book: BookResponse
me?: UserResponse
}

const CardColor = {
Available: 'border-t-green-600',
Loaned: 'border-t-red-600',
Owned: 'border-t-zinc-600',
}

export default function BookCard(properties: BookCardProperties) {
const { book, me } = properties

const isLoaned = !!book.loan
const isLogged = !!me
const isOwned = isLoaned && isLogged && book.loan.user.id === me.id

const cardColor = isOwned
? CardColor.Owned
: isLoaned
? CardColor.Loaned
: CardColor.Available

return (
<>
<Card
className={`max-w-[320px] space-y-4 p-4 group border-t-4 ${cardColor}`}
radius="none"
isHoverable
as={Link}
prefetch
href={`/books/${book.id}`}
>
<div className="relative">
<Image
className="h-[297px] w-[320px] object-cover"
alt={book.title}
width={297}
height={387}
src={book.image}
/>
<BookAvatar book={book} />
<BookCardForm book={book} me={me} />
</div>

<CardBody className="p-0">
<div className="line-clamp-1 font-bold hyphens-auto" lang="en">
{book.title}
</div>
<div className="line-clamp-1 font-extralight text-sm">
{book.authors.join(', ')}
</div>
</CardBody>
</Card>
</>
)
}

function BookAvatar({ book }: { book: BookResponse }) {
if (!book.loan) {
return null
}

return (
<Image
alt="Avatar del poseedor del libro"
src={book.loan.user.image}
height={48}
width={48}
className="absolute top-5 right-5 z-10 rounded-full"
/>
)
}
14 changes: 6 additions & 8 deletions src/components/book-form/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { Divider } from '@nextui-org/react'
import { redirect } from 'next/navigation'
import { useEffect } from 'react'
import { useFormState } from 'react-dom'

Expand All @@ -17,31 +18,28 @@ export default function BookForm() {
useEffect(() => {
if (state?.success) {
showToast('Libro catalogado con éxito')

redirect('/')
}
}, [state])

return (
<>
<form className="flex flex-col gap-4" action={action}>
<InputForm
label="Título"
name="title"
isRequired
errors={state?.errors}
/>
<InputForm label="Título" name="title" isRequired state={state} />
<InputForm
label="Autores"
name="authors"
isRequired
description="Separados por comas"
errors={state?.errors}
state={state}
/>
<InputForm
label="Imagen"
name="image"
isRequired
description="Introduzca la URL de la portada del libro"
errors={state?.errors}
state={state}
/>
<Divider className="col-span-1 md:col-span-2" />
<div className="flex flex-row-reverse">
Expand Down
11 changes: 7 additions & 4 deletions src/components/book-grid/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import Book from '@/components/book'
import BookCard from '@/components/book-card/book-card'
import BookResponse from '@/core/book/dto/responses/book.response'
import UserResponse from '@/core/user/dto/responses/user.response'

export interface BookGridProperties {
books: BookResponse[]
me?: UserResponse
}

export default function BookGrid(properties: BookGridProperties) {
const { books, me } = properties
return (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 place-items-center">
{properties.books.map((book) => (
<Book key={book.id} book={book} />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 place-items-center">
{books.map((book) => (
<BookCard book={book} me={me} key={book.id} />
))}
</div>
</>
Expand Down
Loading

0 comments on commit 5af8ac9

Please sign in to comment.