diff --git a/public/css/filmList.css b/public/css/filmList.css new file mode 100644 index 0000000000000000000000000000000000000000..8f447350d328ab233b39b62b5fbbb15072a1f087 --- /dev/null +++ b/public/css/filmList.css @@ -0,0 +1,48 @@ +.search-bar { + display: flex; + width: 400px; + height: 48px; + padding: 8px 24px; + align-items: center; + gap: 8px; + flex-shrink: 0; + border-radius: 32px; + background: var(--neutral-grey-base, #404650); +} + +.sort-filter { + display: flex; + gap: 12px; +} + +.film-card { + display: flex; + flex-direction: column; + align-items: center; + border-radius: 16px; + background: var(--neutral-grey-dark, #21252C); + width: 210px; + overflow: hidden; +} + +/* Styles for Film Image */ +.film-image { + width: 100%; + height: 280px; + border-radius: 12px 12px 0px 0px; + background-size: cover; + background-repeat: no-repeat; +} + +/* Styles for Film Title */ +.film-title { + color: var(--neutral-white, #FCFCFC); + text-align: center; + font-family: Poppins, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: normal; + flex: 1 0 0; + padding: 12px; +} \ No newline at end of file diff --git a/public/css/styles.css b/public/css/styles.css index b27b3d9eb4bc70f7a2298080b445c6e846d91051..2c7d6525d93d57800909f23f639846a4a3d8aa06 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -24,9 +24,15 @@ time, mark, audio, video { vertical-align: baseline; } +:root { + --neutral-grey-dark: #21252C; + --neutral-white: #FCFCFC; + --neutral-grey-base: #404650; +} * { box-sizing: border-box; + font-family: Poppins, sans-serif; } /* HTML5 display-role reset for older browsers */ @@ -54,7 +60,7 @@ table { } .text { - font-family: 'Open Sans', sans-serif; + font-family: Poppins, sans-serif; } .form-container { diff --git a/public/js/httpClient.js b/public/js/httpClient.js new file mode 100644 index 0000000000000000000000000000000000000000..d50c5fd53c04110211e71ede8ee316f0a0c519c6 --- /dev/null +++ b/public/js/httpClient.js @@ -0,0 +1,45 @@ +class HttpClient { + async promiseAjax(url, payload,method) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + xhr.onload = () => { + try { + const jsonResponse = JSON.parse(xhr.responseText); + resolve(jsonResponse); + } catch (e) { + reject(e); + } + }; + + xhr.onerror = () => { + reject(new Error("Fetch error")); + }; + + const usedMethod = method || "GET"; + const params = new URLSearchParams(payload).toString(); + xhr.open( + usedMethod, + usedMethod !== "GET" ? url : payload ? `${url}?${params}` : url + ); + xhr.setRequestHeader("Content-type", "application/json"); + payload && usedMethod !== "GET" ? xhr.send(JSON.stringify(payload)) : xhr.send(); + }); + } + + async get(url, payload) { + return await this.promiseAjax(url, payload, "GET"); + } + + async post(url, payload) { + return await this.promiseAjax(url, payload, "POST"); + } + + async put(url, payload) { + return await this.promiseAjax(url, payload, "PUT"); + } + + async delete(url) { + return await this.promiseAjax(url, null, "DELETE"); + } +} diff --git a/public/js/utils.js b/public/js/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..2b9b33ea6225af21878b5a0129815698d040ddb7 --- /dev/null +++ b/public/js/utils.js @@ -0,0 +1,11 @@ +class Utils { + debounce(func, timeout = 300) { + let timer; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => { + func.apply(this, args); + }, timeout); + }; + } +} \ No newline at end of file diff --git a/src/App.php b/src/App.php index c6d9097fede08d91b4dd48b05c57c3d248867a23..4967f24232620d06799e260999f890b7fd9d87e8 100644 --- a/src/App.php +++ b/src/App.php @@ -2,6 +2,7 @@ namespace app; +use app\controllers\FilmController; use app\Router; use app\base\BaseController; use app\controllers\CreateFilmController; @@ -35,6 +36,8 @@ class App $this->router->addRoute('/review', ReviewController::class); $this->router->addRoute('/logout', LoginController::class); $this->router->addRoute('/register', RegisterController::class); + $this->router->addRoute('/films', FilmController::class); + $this->router->addRoute('/search', FilmController::class); $this->router->addRoute('/add-film', CreateFilmController::class); $this->router->addRoute('/update-film', UpdateFilmController::class); $this->router->addRoute('/profile', ProfileController::class); diff --git a/src/base/BaseRepository.php b/src/base/BaseRepository.php index a276a5ea411fd9476a3f5e8eedd62c4fb66af76c..9b38df918b0939e2f420d31139d5d874f0200bfb 100644 --- a/src/base/BaseRepository.php +++ b/src/base/BaseRepository.php @@ -150,11 +150,11 @@ abstract class BaseRepository } } - if ($pageSize && $pageNo) { + if (isset($pageSize) && isset($pageNo)) { $offset = $pageSize * ($pageNo - 1); $stmt->bindValue(":pageSize", $pageSize, PDO::PARAM_INT); - $stmt->bindValue(":offset", $offset, PDO::PARAM_INT); + $stmt->bindValue(":pageNo", $offset, PDO::PARAM_INT); } $stmt->execute(); @@ -178,7 +178,6 @@ abstract class BaseRepository } // Hydrating statement, for sanitizing - // echo $sql; $stmt = $this->pdo->prepare($sql); @@ -271,6 +270,12 @@ abstract class BaseRepository public function getDistinctValues($columnName) { $sql = "SELECT DISTINCT $columnName FROM $this->tableName"; - return $this->pdo->query($sql); + $stmt = $this->pdo->query($sql); + + if ($stmt) { + return $stmt->fetchAll(PDO::FETCH_COLUMN); + } else { + return []; + } } } diff --git a/src/controllers/FilmController.php b/src/controllers/FilmController.php new file mode 100644 index 0000000000000000000000000000000000000000..c88a14a7f21e8f10c4c290ca484adf76292bf8ab --- /dev/null +++ b/src/controllers/FilmController.php @@ -0,0 +1,48 @@ +<?php + +namespace app\controllers; + +use app\base\BaseController; +use app\Request; +use app\services\FilmService; + +class FilmController extends BaseController +{ + public function __construct() + { + parent::__construct(FilmService::getInstance()); + } + + protected function get($urlParams) + { + $uri = Request::getURL(); + $page = (isset($_GET['page']) and (int) $_GET['page'] >= 1) ? $_GET['page'] : 1; + $word = $_GET['q'] ?? ""; + $genre = $_GET['genre'] ?? 'all'; + $released_year = $_GET['year'] ?? 'all'; + $isDesc = $_GET['desc'] ?? false; + $order = $_GET['order'] ?? 'title'; + $data = $this->service->searchAndFilter($word, $order, $isDesc, $genre, $released_year, $page); + + if ($uri == "/films") + { + $data['genres'] = $this->service->getAllCategoryValues('genre'); + $data['released_years'] = $this->service->getAllCategoryValues('released_year'); + + parent::render($data, 'filmList', "layouts/base"); + } + else + { + $films = []; + + foreach ($data['films'] as $film) + { + $films[] = $film->toResponse(); + } + $data['films'] = $films; + + send_json_response($data); + } + } + +} diff --git a/src/controllers/utils/response.php b/src/controllers/utils/response.php new file mode 100644 index 0000000000000000000000000000000000000000..1c2d74822b5a862d8f8e519790eea0d4ab18c261 --- /dev/null +++ b/src/controllers/utils/response.php @@ -0,0 +1,12 @@ +<?php +function send_json_response($data = [], $status_code = 200) { + http_response_code($status_code); + header('Content-Type: application/json'); + + $response = [ + 'status' => $status_code, + 'data' => $data, + ]; + + echo json_encode($response); +} \ No newline at end of file diff --git a/src/repositories/FilmRepository.php b/src/repositories/FilmRepository.php index 3cdbf7ab1f36753613ef612738ca1dd3a3c30257..bec39a177167a74ad20bc7b77169436cae4f7552 100644 --- a/src/repositories/FilmRepository.php +++ b/src/repositories/FilmRepository.php @@ -28,16 +28,10 @@ class FilmRepository extends BaseRepository return $this->findOne(['film_id' => [$film_id, PDO::PARAM_INT]]); } - public function getAllBySearchAndFilter( - $word, - $order = 'title', - $isDesc = false, - $genre = 'all', - $released_year = 'all', - $pageNo = 1, - $limit = PAGINATION_LIMIT - ) { - $where = []; + public function getAllBySearchAndFilter($word, $order = 'title', $isDesc= false, $genre = 'all', + $released_year = 'all', $pageNo = 1, $limit = 10) + { + $where = []; if (isset($genre) and !empty($genre) and $genre != 'all') { $where['genre'] = [$genre, PDO::PARAM_STR, 'LIKE']; @@ -52,6 +46,23 @@ class FilmRepository extends BaseRepository return $this->findAll($where, $order, $pageNo, $limit, $isDesc); } + public function countRowBySearchAndFilter($word, $genre = 'all', $released_year = 'all') + { + $where = []; + + if (isset($genre) and !empty($genre) and $genre != 'all') { + $where['genre'] = [$genre, PDO::PARAM_STR, 'LIKE']; + } + if (isset($released_year) and !empty($released_year) and $released_year != 'all') { + $where['released_year'] = [$released_year, PDO::PARAM_INT]; + } + if (isset($word) and !empty($word)) { + $where['title'] = [$genre, PDO::PARAM_STR, 'LIKE', ['director']]; + } + + return $this->countRow($where); + } + public function getAllCategoryValues($category) { return $this->getDistinctValues($category); diff --git a/src/services/FilmService.php b/src/services/FilmService.php index de99aad9df6d7704dae16e2220d3b9415e262e69..9bc6307cf8c3d2ca299b8d6b93d798c7bfa557f4 100644 --- a/src/services/FilmService.php +++ b/src/services/FilmService.php @@ -84,4 +84,30 @@ class FilmService extends BaseService return $this->repository->update($film, $arrParams); } + + + public function searchAndFilter($word, $order, $isDesc, $genre, $released_year, $page = 1) + { + $data = null; + $word = strtolower(trim($word)); + $response = $this->repository->getAllBySearchAndFilter($word, $order, $isDesc, $genre, $released_year , $page); + $films = []; + foreach ($response as $resp) { + $film = new FilmModel(); + $films[] = $film->constructFromArray($resp); + } + $data['films'] = $films; + + $row_count = $this->repository->countRowBySearchAndFilter($word, $genre, $released_year); + $total_page = ceil($row_count/10); + $data['total_page'] = $total_page; + + return $data; + } + + public function getAllCategoryValues($category) + { + return $this->repository->getAllCategoryValues($category); + } + } diff --git a/views/filmList.php b/views/filmList.php new file mode 100644 index 0000000000000000000000000000000000000000..6660afd0c29e8267937ad6b9d2676c3a45dfc333 --- /dev/null +++ b/views/filmList.php @@ -0,0 +1,69 @@ +<div> + <div class="search-bar"> + <input type="text" id="search-input" placeholder="Search film title or director"> + </div> + + <div class="sort-filter"> + <select id="sort-by"> + <option value="title">Sort by Title</option> + <option value="released-year">Sort by Released Year</option> + </select> + + <select id="sort-order"> + <option value="ascending">Ascending</option> + <option value="descending">Descending</option> + </select> + + <select id="filter-genre"> + <option value="" disabled selected>Choose Genre</option> + <?php + if (isset($data['genres'])) + { + foreach ($data['genres'] as $genre) { + echo "<option value='$genre'>$genre</option>"; + } + } + ?> + </select> + + <select id="filter-year"> + <option value="" disabled selected>Choose Released Year</option> + <?php + if (isset($data['released_years'])) + { + foreach ($data['released_years'] as $year) { + echo "<option value='$year'>$year</option>"; + } + } + ?> + </select> + </div> + + <div class="film-card-container"> + <?php + if (isset($data['films'])) + { + foreach ($data['films'] as $film) { + echo "<div class='film-card'> + <div class='film-image' style='background-image: url($film->image_path);'></div> + <div class='film-title'> $film->title </div> + </div>"; + } + } + ?> + </div> + + <div class="pagination"> + <button id="prev-page">Previous</button> + <?php + $totalPages = $data['total_page']; + $currentPage = $_GET['page'] ?? 1; + + for ($i = 1; $i <= $totalPages; $i++) { + echo "<a href='?page=$i' id='current-page' class='page-number " . ($i == $currentPage ? 'active' : '') . "'>$i</a>"; + } + ?> + <button id="next-page">Next</button> + </div> + <script></script> +</div> \ No newline at end of file diff --git a/views/layouts/base.php b/views/layouts/base.php index e602316656d6b08fb73725225dc16efd44d1da87..34170daa4bc9712813d3e84c69a14f2f32e0103f 100644 --- a/views/layouts/base.php +++ b/views/layouts/base.php @@ -7,12 +7,10 @@ <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel='stylesheet' href='/public/css/navbar.css'> <link rel='stylesheet' href='/public/css/styles.css'> + <link rel='stylesheet' href='/public/css/filmList.css'> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> - <link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap" rel="stylesheet"> - <link rel="preconnect" href="https://fonts.googleapis.com"> - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> - <link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet"> + <link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400;1,500;1,600;1,700;1,800&display=swap" rel="stylesheet"> <!-- <link rel="stylesheet" href="public/css/lib.css"> <link rel="stylesheet" href="public/css/shared.css"> <link rel="stylesheet" href="public/css/home.css"> -->