Skip to content
This repository has been archived by the owner on Nov 5, 2024. It is now read-only.

Commit

Permalink
Add Book page
Browse files Browse the repository at this point in the history
  • Loading branch information
kkamara committed Oct 23, 2024
1 parent bc1560b commit 27666d8
Show file tree
Hide file tree
Showing 23 changed files with 509 additions and 24 deletions.
22 changes: 22 additions & 0 deletions app/Http/Controllers/V1/Web/BookController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace App\Http\Controllers\V1\Web;

use App\Enums\V1\BookApproved;
use App\Http\Controllers\Controller;
use App\Http\Resources\V1\BookResource;
use App\Models\V1\Book;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class BookController extends Controller
{
public function get(Request $request, Book $book) {
if ($book->approved !== BookApproved::APPROVED) {
return response()->json([
"message" => "Resource not found.",
], Response::HTTP_NOT_FOUND);
}
return new BookResource($book);
}
}
26 changes: 26 additions & 0 deletions app/Http/Controllers/V1/Web/ReviewController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace App\Http\Controllers\V1\Web;

use App\Enums\V1\BookApproved;
use App\Http\Controllers\Controller;
use App\Http\Resources\V1\ReviewCollection;
use App\Models\V1\Book;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class ReviewController extends Controller
{
public function getReviewByBook(Request $request, Book $book) {
if ($book->approved !== BookApproved::APPROVED) {
return response()->json([
"message" => "Resource not found.",
], Response::HTTP_NOT_FOUND);
}
return new ReviewCollection(
$book->reviews()
->where("approved", 1)
->paginate(3)
);
}
}
3 changes: 2 additions & 1 deletion app/Http/Resources/V1/BookResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ class BookResource extends JsonResource
public function toArray(Request $request): array
{
return [
"id" => $this->id,
"isbn13" => $this->isbn_13,
"isbn10" => $this->isbn_10,
"user" => $this->user,
"user" => new UserResource($this->user),
"name" => $this->name,
"description" => $this->description,
"jpgImageURL" => $this->jpg_image_url,
Expand Down
19 changes: 19 additions & 0 deletions app/Http/Resources/V1/ReviewCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\Http\Resources\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;

class ReviewCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
}
26 changes: 26 additions & 0 deletions app/Http/Resources/V1/ReviewResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace App\Http\Resources\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ReviewResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
"id" => $this->id,
"rating" => $this->rating,
"text" => $this->text,
"user" => new UserResource($this->user),
"createdAt" => $this->created_at,
"updatedAt" => $this->created_at,
];
}
}
1 change: 1 addition & 0 deletions app/Http/Resources/V1/UserResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class UserResource extends JsonResource
public function toArray($request)
{
return [
"id" => $this->id,
"name" => $this->name,
"email" => $this->email,
"createdAt" => $this->created_at,
Expand Down
4 changes: 4 additions & 0 deletions resources/js/Routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import Home from "./components/pages/HomeComponent"
import Login from "./components/pages/auth/LoginComponent"
import Logout from "./components/pages/auth/LogoutComponent"
import Register from "./components/pages/auth/RegisterComponent"
import Book from "./components/pages/book/BookComponent"
import NotFound from "./components/pages/http/NotFoundComponent"

import { url } from './utils/config'

Expand All @@ -17,6 +19,8 @@ export default () => {
<Header/>
<Routes>
<Route path={url("/")} element={<Home />}/>
<Route path={url("/notfound")} element={<NotFound />}/>
<Route path={url("/books/:book")} element={<Book />}/>
<Route path={url("/user/login")} element={<Login />}/>
<Route path={url("/user/logout")} element={<Logout />}/>
<Route path={url("/user/register")} element={<Register />}/>
Expand Down
1 change: 1 addition & 0 deletions resources/js/components/layouts/Footer.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.app-footer {
margin-bottom: 30px;
margin-top: 30px;
}
4 changes: 2 additions & 2 deletions resources/js/components/pages/HomeComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export default function HomeComponent() {
<div className="col-md-12">
{state.home.data.data.map((book, index) => (
<div key={index} className="card home-card">
<a href="#">
<a href={`/books/${book.id}`}>
<img src={book.jpgImageURL} className="card-img-top" alt="..." />
</a>
<div className="card-body">
Expand All @@ -85,7 +85,7 @@ export default function HomeComponent() {
<span className="card-span">Published {book.published}</span>
<span className="card-span book-cost">£{book.cost}</span>
</p>
<a href="#" className="btn btn-primary">
<a href={`/books/${book.id}`} className="btn btn-primary">
View Book
</a>
</div>
Expand Down
4 changes: 0 additions & 4 deletions resources/js/components/pages/HomeComponent.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
.home-item {
text-align: left;
}

.home-card {
width: 18rem;
display: inline-block !important;
Expand Down
169 changes: 169 additions & 0 deletions resources/js/components/pages/book/BookComponent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React, { useEffect, useState, } from 'react'
import { useDispatch, useSelector, } from 'react-redux'
import { useParams, useNavigate, } from 'react-router'
import moment from 'moment'
import ReactPaginate from 'react-paginate'
import { getBook, } from '../../../redux/actions/bookActions'
import { getReviews, } from '../../../redux/actions/reviewsActions'

import "./BookComponent.scss"

export default function BookComponent() {
const dispatch = useDispatch()
const state = useSelector(state => ({
book: state.book,
reviews: state.reviews,
}))
let { book: bookId } = useParams()
const navigate = useNavigate()
const [showReviews, setShowReviews] = useState("")

useEffect(() => {
dispatch(getBook(bookId))
}, [])

useEffect(() => {
if (
!state.book.loading &&
typeof state.book.data === 'object' &&
null !== state.book.data
) {
dispatch(getReviews(bookId))
}
}, [state.book])

useEffect(() => {
if (state.book.error !== null) {
return navigate("/notfound")
}
}, [state.book])

const reviewTitle = () => {
let reviewAverage = null
if (state.book.data.data.reviewAverage) {
reviewAverage = state.book.data.data.reviewAverage
} else {
return "Reviews"
}
return `Reviews (${reviewAverage})`
}

const handlePageChange = ({ selected, }) => {
const newPage = selected + 1
if (selected > state.reviews.data.last_page) {
return
}
dispatch(getReviews(bookId, newPage))
setShowReviews("show")
}

const pagination = () => {
if (!state.reviews.data) {
return null
}

return <div className="review-pagination">
<ReactPaginate
onPageChange={handlePageChange}
previousLabel="Previous"
nextLabel="Next"
pageClassName="page-item"
pageLinkClassName="page-link"
previousClassName="page-item"
previousLinkClassName="page-link"
nextClassName="page-item"
nextLinkClassName="page-link"
breakLabel="..."
breakClassName="page-item"
breakLinkClassName="page-link"
pageCount={state.reviews.data.meta.last_page}
marginPagesDisplayed={2}
pageRangeDisplayed={5}
containerClassName="pagination"
activeClassName="active"
forcePage={state.reviews.data.meta.current_page - 1}
/>
</div>
}

const paginationDetail = () => {
return <div className="text-center">
<strong>page</strong> ({state.reviews.data.meta.current_page}),
&nbsp;<strong>page count</strong> ({state.reviews.data.meta.last_page}),
&nbsp;<strong>displayed items</strong> ({state.reviews.data.data.length}),
&nbsp;<strong>items</strong> ({state.reviews.data.meta.total})
</div>
}

const renderReviews = () => {
if (!state.reviews.data) {
return null
}
return (
<>
{paginationDetail()}
{state.reviews.data.data.map((review, index) => (
<div key={index} className="card card-body review-card-body">
<p>Rated <span className="rating">{review.rating}</span></p>
<p>{review.text}</p>
<p className="float-right">
Submitted {parseDate(review.createdAt)} by {review.user.name}
</p>
</div>
))}
{paginationDetail()}
</>
)
}

const parseDate = date => moment(date).format('YYYY-MM-DD hh:mm')

if (
!state.book.loading &&
typeof state.book.data === 'object' &&
null !== state.book.data
) {
console.log('book', state.book.data)
}
if (state.book.loading || state.reviews.loading) {
return <div className="container book-container text-center">
<p>Loading...</p>
</div>
}

return (
<>
<div className='container book-container'>
<h1>{state.book.data.data.name}</h1>
<img
src={state.book.data.data.jpgImageURL}
alt={state.book.data.data.name}
className="book-cover"
/>
<div className="col-md-12 book-detail">
<span className="book-cost">£{state.book.data.data.cost}</span> <button className="btn btn-primary add-to-cart">Add to cart</button>
</div>
<div className="col-md-12 review-container">
<p className="d-inline-flex gap-1">
<button className="btn btn-info" type="button" data-bs-toggle="collapse" data-bs-target="#descriptionCollapse" aria-expanded="false" aria-controls="descriptionCollapse">
Description
</button>
<button className="btn btn-info" type="button" data-bs-toggle="collapse" data-bs-target="#reviewCollapse" aria-expanded="false" aria-controls="reviewCollapse">
{reviewTitle()}
</button>
</p>
<div className="collapse description-collapse" id="descriptionCollapse">
<div className="card card-body">
{state.book.data.data.description}
</div>
</div>
<div className={`collapse book-collapse ${showReviews}`} id="reviewCollapse">
{pagination()}
{renderReviews()}
{pagination()}
</div>
</div>
</div>
</>
)
}
43 changes: 43 additions & 0 deletions resources/js/components/pages/book/BookComponent.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
.book-cover {
max-width: 350px;
border-radius: 50px;
}

.review-container {
margin-top: 30px;
}

.book-cost {
font-size: 18px;
font-weight: 700;
}

.book-detail {
margin-top: 30px;
}

.add-to-cart {
margin-left: 10px;
}

.book-collapse {
text-align: left;
}

.rating {
font-weight: 700;
text-decoration: underline;
}

.review-card-body {
margin-bottom: 10px;
}

.description-collapse {
text-align: left;
}

.review-pagination {
display: flex;
justify-content: center;
}
13 changes: 13 additions & 0 deletions resources/js/components/pages/http/NotFoundComponent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React, { useEffect, } from 'react'
import moment from 'moment'

export default function NotFoundComponent() {
console.log(1)
return (
<>
<div className='container not-found-container'>
<h1><pre>404 | Not Found</pre></h1>
</div>
</>
)
}
Loading

0 comments on commit 27666d8

Please sign in to comment.