Skip to content

Commit

Permalink
Merge pull request #108 from leepeuker/movies-page
Browse files Browse the repository at this point in the history
Movies page
  • Loading branch information
leepeuker authored Sep 15, 2022
2 parents c682897 + 23bb142 commit 11ab711
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 6 deletions.
11 changes: 11 additions & 0 deletions public/js/movies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
function toggleSearchOptions () {
let searchOptions = document.getElementById('searchOptions')

if (searchOptions.style.display === 'none') {
searchOptions.style.display = 'block'

return
}

searchOptions.style.display = 'none'
}
5 changes: 5 additions & 0 deletions settings/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
'/{username:[a-zA-Z0-9]+}/history',
[\Movary\HttpController\HistoryController::class, 'renderHistory']
);
$routeCollector->addRoute(
'GET',
'/{username:[a-zA-Z0-9]+}/movies',
[\Movary\HttpController\MoviesController::class, 'renderPage']
);
$routeCollector->addRoute(
'POST',
'/refresh-trakt',
Expand Down
10 changes: 10 additions & 0 deletions src/Application/Movie/Api.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ public function fetchHistoryOrderedByWatchedAtDesc(int $userId) : array
return $this->historySelectService->fetchHistoryOrderedByWatchedAtDesc($userId);
}

public function fetchUniqueMoviesCount(int $userId, ?string $searchTerm) : int
{
return $this->historySelectService->fetchUniqueMovieInHistoryCount($userId, $searchTerm);
}

public function fetchUniqueMoviesPaginated(int $userId, int $limit, int $page, ?string $searchTerm, string $sortBy, string $sortOrder) : array
{
return $this->historySelectService->fetchUniqueMoviesPaginated($userId, $limit, $page, $searchTerm, $sortBy, $sortOrder);
}

public function fetchWithActor(int $personId, int $userId) : array
{
return $this->movieSelectService->fetchWithActor($personId, $userId);
Expand Down
15 changes: 13 additions & 2 deletions src/Application/Movie/History/Service/Select.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,20 @@ public function fetchTotalHoursWatched(int $userId) : int
return (int)round($minutes / 60);
}

public function fetchUniqueMovieInHistoryCount(int $userId) : int
public function fetchUniqueMovieInHistoryCount(int $userId, ?string $searchTerm = 'null') : int
{
return $this->movieRepository->fetchUniqueMovieInHistoryCount($userId);
return $this->movieRepository->fetchUniqueMovieInHistoryCount($userId, $searchTerm);
}

public function fetchUniqueMoviesPaginated(
int $userId,
int $limit,
int $page,
?string $searchTerm = null,
string $sortBy = 'title',
string $sortOrder = 'ASC'
) : array {
return $this->movieRepository->fetchUniqueMoviesPaginated($userId, $limit, $page, $searchTerm, $sortBy, $sortOrder);
}

public function findByTraktId(TraktId $traktId) : ?Entity
Expand Down
48 changes: 44 additions & 4 deletions src/Application/Movie/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -430,14 +430,54 @@ public function fetchTotalMinutesWatched(int $userId) : int
)->fetchFirstColumn()[0];
}

public function fetchUniqueMovieInHistoryCount(int $userId) : int
public function fetchUniqueMovieInHistoryCount(int $userId, ?string $searchTerm) : int
{
if ($searchTerm !== null) {
return $this->dbConnection->fetchFirstColumn(
<<<SQL
SELECT COUNT(DISTINCT movie_id)
FROM movie_user_watch_dates mh
JOIN movie m on mh.movie_id = m.id
WHERE m.title LIKE ? AND user_id = ?
SQL,
["%$searchTerm%", $userId]
)[0];
}

return $this->dbConnection->executeQuery(
'SELECT COUNT(DISTINCT movie_id) FROM movie_user_watch_dates WHERE user_id = ?',
[$userId]
)->fetchFirstColumn()[0];
}

public function fetchUniqueMoviesPaginated(int $userId, int $limit, int $page, ?string $searchTerm, string $sortBy, string $sortOrder) : array
{
$payload = [$userId, $userId, "%$searchTerm%"];

$offset = ($limit * $page) - $limit;

$sortBySanitized = match ($sortBy) {
'rating' => 'rating',
'releaseDate' => 'release_date',
'runtime' => 'runtime',
default => 'title'
};

return $this->dbConnection->fetchAllAssociative(
<<<SQL
SELECT m.*, mur.rating as userRating
FROM movie m
JOIN movie_user_watch_dates mh on mh.movie_id = m.id and mh.user_id = ?
LEFT JOIN movie_user_rating mur ON mh.movie_id = mur.movie_id and mur.user_id = ?
WHERE m.title LIKE ?
GROUP BY m.id, title, release_date, rating
ORDER BY $sortBySanitized $sortOrder
LIMIT $offset, $limit
SQL,
$payload
);
}

public function fetchWithActor(int $personId, int $userId) : array
{
return $this->dbConnection->fetchAllAssociative(
Expand Down Expand Up @@ -517,9 +557,9 @@ public function findPlaysForMovieIdAndDate(int $movieId, int $userId, Date $watc
public function findUserRating(int $movieId, int $userId) : ?PersonalRating
{
$userRating = $this->dbConnection->fetchFirstColumn(
'SELECT rating FROM `movie_user_rating` WHERE movie_id = ? AND user_id = ?',
[$movieId, $userId]
)[0] ?? null;
'SELECT rating FROM `movie_user_rating` WHERE movie_id = ? AND user_id = ?',
[$movieId, $userId]
)[0] ?? null;

return $userRating !== null ? PersonalRating::create($userRating) : null;
}
Expand Down
65 changes: 65 additions & 0 deletions src/HttpController/MoviesController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php declare(strict_types=1);

namespace Movary\HttpController;

use Movary\Application\Movie;
use Movary\Application\User\Service\UserPageAuthorizationChecker;
use Movary\ValueObject\Http\Request;
use Movary\ValueObject\Http\Response;
use Movary\ValueObject\Http\StatusCode;
use Twig\Environment;

class MoviesController
{
private const DEFAULT_LIMIT = 24;

private const DEFAULT_SORT_BY = 'title';

private const DEFAULT_SORT_ORDER = 'DESC';

public function __construct(
private readonly Environment $twig,
private readonly Movie\Api $movieApi,
private readonly UserPageAuthorizationChecker $userPageAuthorizationChecker,
) {
}

public function renderPage(Request $request) : Response
{
$userId = $this->userPageAuthorizationChecker->findUserIdIfCurrentVisitorIsAllowedToSeeUser((string)$request->getRouteParameters()['username']);
if ($userId === null) {
return Response::createNotFound();
}

$searchTerm = $request->getGetParameters()['s'] ?? null;
$page = $request->getGetParameters()['p'] ?? 1;
$limit = $request->getGetParameters()['pp'] ?? self::DEFAULT_LIMIT;
$sortBy = $request->getGetParameters()['sb'] ?? self::DEFAULT_SORT_BY;
$sortOrder = $request->getGetParameters()['so'] ?? self::DEFAULT_SORT_ORDER;

$uniqueMovies = $this->movieApi->fetchUniqueMoviesPaginated($userId, (int)$limit, (int)$page, $searchTerm, $sortBy, $sortOrder);
$historyCount = $this->movieApi->fetchUniqueMoviesCount($userId, $searchTerm);

$maxPage = (int)ceil($historyCount / $limit);

$paginationElements = [
'previous' => $page > 1 ? $page - 1 : null,
'next' => $page < $maxPage ? $page + 1 : null,
'currentPage' => $page,
'maxPage' => $maxPage,
];

return Response::create(
StatusCode::createOk(),
$this->twig->render('page/movies.html.twig', [
'users' => $this->userPageAuthorizationChecker->fetchAllVisibleUsernamesForCurrentVisitor(),
'movies' => $uniqueMovies,
'paginationElements' => $paginationElements,
'searchTerm' => $searchTerm,
'perPage' => $limit,
'sortBy' => $sortBy,
'sortOrder' => $sortOrder,
]),
);
}
}
1 change: 1 addition & 0 deletions templates/component/navbar.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
{% endif %}
<li><a class="dropdown-item {% if requestUrlPath matches '#\/.+\/dashboard#' %}active{% endif %}" href="/{{ routeUsername }}/dashboard">Dashboard</a></li>
<li><a class="dropdown-item {% if requestUrlPath matches '#\/.+\/history#' %}active{% endif %}" href="/{{ routeUsername }}/history">History</a></li>
<li><a class="dropdown-item {% if requestUrlPath matches '#\/.+\/movies#' %}active{% endif %}" href="/{{ routeUsername }}/movies">All Movies</a></li>
<li><a class="dropdown-item {% if requestUrlPath matches '#\/.+\/most-watched-actors#' %}active{% endif %}" href="/{{ routeUsername }}/most-watched-actors">Top Actors</a></li>
<li><a class="dropdown-item {% if requestUrlPath matches '#\/.+\/most-watched-directors#' %}active{% endif %}" href="/{{ routeUsername }}/most-watched-directors">Top Directors</a></li>
<li>
Expand Down
115 changes: 115 additions & 0 deletions templates/page/movies.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{% extends 'base.html.twig' %}
{% import 'makro/tmdb.html.twig' as tmdb %}

{% block title %}
History
{% endblock %}

{% block stylesheets %}
<link href="/css/app.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
{% endblock %}

{% block scripts %}
<script src="/js/userSelect.js"></script>
<script src="/js/movies.js"></script>
{% endblock %}

{% block body %}
<main role="main" class="container">
{{ include('component/navbar.html.twig') }}

<div style="text-align: center">
{{ include('component/user-select.html.twig') }}

<form action="/{{ routeUsername }}/movies" method="GET">
<div class="input-group mb-3">
<input type="text" class="form-control" name="s" placeholder="Search" value="{{ (searchTerm is null) ? '' : searchTerm }}">
<button class="btn btn-primary" type="button" onclick="toggleSearchOptions()" style="background-color: #aab0b3;border-color: #aab0b3"><i
class="bi bi-chevron-down"></i></button>
<button class="btn btn-primary" type="submit"><i class="bi bi-search"></i></button>
</div>
<div style="display: none;border: #aab0b3 1px;margin-top:1em;margin-bottom:1em" id="searchOptions">
<div class="input-group mb-3">
<span class="input-group-text">Per page</span>
<select class="form-control" name="pp" id="per-page">
<option value="24" {{ perPage == 24 ? 'selected' }}>24</option>
<option value="48" {{ perPage == 48 ? 'selected' }}>48</option>
<option value="72" {{ perPage == 72 ? 'selected' }}>72</option>
<option value="96" {{ perPage == 96 ? 'selected' }}>96</option>
</select>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Sort by</span>
<select class="form-control" name="sb" id="sort-by">
<option value="title" {{ sortBy == 'title' ? 'selected' }}>Title</option>
<option value="releaseDate" {{ sortBy == 'releaseDate' ? 'selected' }}>Release date</option>
<option value="rating" {{ sortBy == 'rating' ? 'selected' }}>Rating</option>
<option value="runtime" {{ sortBy == 'runtime' ? 'selected' }}>Runtime</option>
</select>
</div>
<div class="input-group mb-3">
<span class="input-group-text">Sort order</span>
<select class="form-control" name="so" id="sort-order">
<option value="desc" {{ sortOrder == 'desc' ? 'selected' }}>Desc</option>
<option value="asc" {{ sortOrder == 'asc' ? 'selected' }}>Asc</option>
</select>
</div>
</div>
</form>

<div class="row row-cols-3 row-cols-md-3 row-cols-lg-6">
{% for movie in movies %}
<div class="col" style="padding-bottom: 1rem;cursor: pointer" onclick="window.location='/{{ routeUsername }}/movie/{{ movie.id }}'">
<div class="card h-100 position-relative">
<img src="{{ tmdb.generatePosterImageUrl(movie.tmdb_poster_path) }}" class="card-img-top" alt="...">

{% if movie.userRating is not null %}
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning text-dark" style="font-size: 0.8rem">
{{ movie.userRating }}
</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>

<ul class="pagination justify-content-center">
{% if paginationElements.previous is null %}
<li class="page-item disabled"><p class="page-link"><span aria-hidden="true">&laquo;</span></p></li>
<li class="page-item disabled"><p class="page-link"><span aria-hidden="true">&lsaquo;</span></p></li>
{% else %}
<li class="page-item">
<a class="page-link" href="/{{ routeUsername }}/history?{{ (searchTerm is null) ? '' : "s=#{searchTerm}&" }}p=1">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
<li class="page-item">
<a class="page-link" href="/{{ routeUsername }}/history?{{ (searchTerm is null) ? '' : "s=#{searchTerm}&" }}p={{ paginationElements.previous }}">
<span aria-hidden="true">&lsaquo;</span>
</a>
</li>
{% endif %}
<li class="page-item active">
<p class="page-link">{{ paginationElements.currentPage }} of {{ paginationElements.maxPage }}</p>
</li>
{% if paginationElements.next is null %}
<li class="page-item disabled"><p class="page-link"><span aria-hidden="true">&rsaquo;</span></p></li>
<li class="page-item disabled"><p class="page-link"><span aria-hidden="true">&raquo;</span></p></li>
{% else %}
<li class="page-item">
<a class="page-link" href="/{{ routeUsername }}/history?{{ (searchTerm is null) ? '' : "s=#{searchTerm}&" }}p={{ paginationElements.next }}">
<span aria-hidden="true">&rsaquo;</span>
</a>
</li>
<li class="page-item">
<a class="page-link" href="/{{ routeUsername }}/history?{{ (searchTerm is null) ? '' : "s=#{searchTerm}&" }}p={{ paginationElements.maxPage }}">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% endif %}
</ul>

</div>
</main>
{% endblock %}

0 comments on commit 11ab711

Please sign in to comment.