diff --git a/public/css/filmList.css b/public/css/filmList.css index 8f447350d328ab233b39b62b5fbbb15072a1f087..fe82a6f21876364763bcf4eb57e9e28b1ee7fba6 100644 --- a/public/css/filmList.css +++ b/public/css/filmList.css @@ -15,6 +15,17 @@ gap: 12px; } +.film-card-container { + display: flex; + flex-wrap: wrap; + gap: 24px; + justify-content: flex-start; + align-items: flex-start; + padding: 30px 0px 30px 0px; + border-radius: 8px; + margin: 5% auto; +} + .film-card { display: flex; flex-direction: column; @@ -25,7 +36,6 @@ overflow: hidden; } -/* Styles for Film Image */ .film-image { width: 100%; height: 280px; @@ -34,7 +44,6 @@ background-repeat: no-repeat; } -/* Styles for Film Title */ .film-title { color: var(--neutral-white, #FCFCFC); text-align: center; diff --git a/public/css/profile.css b/public/css/profile.css new file mode 100644 index 0000000000000000000000000000000000000000..ee7f2d6cfb7015a44fdfc6afe3ea017c86539bd0 --- /dev/null +++ b/public/css/profile.css @@ -0,0 +1,45 @@ +.tab { + padding: 10px 20px; + text-decoration: none; + color: var(--neutral-white, #FCFCFC); + font-weight: normal; + transition: color 0.2s ease, border-bottom 0.2s ease, font-weight 0.2s ease; +} + +.active-tab { + border-bottom: 2px solid #333; + font-weight: bold; + color: var(--neutral-white, #FCFCFC); +} + +.tab:hover { + color: var(--primary-base); +} + +.tab-container { + display: flex; + justify-content: center; + align-items: center; + margin: auto; + gap: 24px; +} + +.reviews-container { + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + padding: 12px 0px 12px 0px; + width: 80%; + margin: auto; +} + +.favorites-container { + display: flex; + flex-wrap: wrap; + gap: 24px; + justify-content: flex-start; + align-items: flex-start; + padding: 12px 0px 12px 0px; + border-radius: 8px; + margin: auto; +} \ No newline at end of file diff --git a/public/js/profile.js b/public/js/profile.js new file mode 100644 index 0000000000000000000000000000000000000000..b25975bb2074be0c7e74b29113c1770cc645ee34 --- /dev/null +++ b/public/js/profile.js @@ -0,0 +1,157 @@ +const tabs = document.querySelectorAll('.tab'); + +tabs.forEach(tab => { + tab.addEventListener('click', (e) => { + e.preventDefault(); + const tabName = tab.getAttribute('data-tab'); + setActiveTab(tabName); + loadTabContent(tabName); + }); +}); + +function handleTabClick(event) { + if (!event.target.classList.contains('tab')) return; + + event.preventDefault(); + + const tabName = event.target.getAttribute('data-tab'); + setActiveTab(tabName); + loadTabContent(tabName); +} + +function setActiveTab(tabName) { + document.querySelectorAll('.tab').forEach(tab => { + tab.classList.remove('active-tab'); + }); + const activeTab = document.querySelector(`[data-tab="${tabName}"]`); + activeTab.classList.add('active-tab'); +} + +// Get content containers +const profileContainer = document.getElementById("profile-container"); +const favoritesContainer = document.getElementById("favorites-container"); +const reviewsContainer = document.getElementById("reviews-container"); + +// Function to show/hide containers based on tab clicks +function showProfile() { + profileContainer.style.display = "block"; + favoritesContainer.style.display = "none"; + reviewsContainer.style.display = "none"; +} + +function showFavorites() { + profileContainer.style.display = "none"; + favoritesContainer.style.display = "flex"; + reviewsContainer.style.display = "none"; +} + +function showReviews() { + profileContainer.style.display = "none"; + favoritesContainer.style.display = "none"; + reviewsContainer.style.display = "block"; +} + +async function userFavAndReviewHandler(link) { + try + { + const httpClient = new HttpClient(); + httpClient.get(link).then( + (response) => { + if (response.status === 200) { + const responseData = response.data; + if (link === "/my-favorites") + { + updateFavorites(responseData['films']) + } + else if (link === "/my-reviews") + { + updateReviews(responseData['reviews'], responseData['username']) + } + } else { + console.error("Error:", response); + } + } + ) + } catch (e) + { + console.error("Error: ", e) + } +} + +function updateFavorites(films) { + favoritesContainer.innerHTML = "" + + favoritesContainer.innerHTML = films.map((film) => ` + <div class='film-card'> + <div class='film-image' style='background-image: url(${film.image_path});'></div> + <div class='film-title'>${film.title}</div> + </div> + `).join(''); +} + +function updateReviews(reviews, username) { + reviewsContainer.innerHTML = ''; + + for (const review of reviews) { + const { rating, notes, published_time} = review; + const timestamp = Date.parse(published_time); + const formatted_time = new Date(timestamp).toLocaleString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: 'numeric', + minute: 'numeric' + }); + + const reviewElement = document.createElement('div'); + reviewElement.classList.add('review'); + + reviewElement.innerHTML = ` + <form class='review-form'> + <div class='review-group'> + <div class='review-info'> + <div class='loop'> + ${getRatingStars(rating)} + </div> + <p>by ${username}</p> + </div> + <h3 class='review-result'>${notes}</h3> + <h3 class='time'>${formatted_time}</h3> + </div> + </form> + `; + + reviewsContainer.appendChild(reviewElement); + } +} + +function getRatingStars(rating) { + let stars = ''; + for (let i = 0; i < 5; i++) { + stars += `<span class="icon-rating ${i < rating ? 'active' : ''}">★</span>`; + } + return stars; +} + +async function loadTabContent(tabName) { + switch (tabName) { + case 'profile': + showProfile(); + break; + case 'favorites': + await userFavAndReviewHandler("/my-favorites"); + showFavorites(); + break; + case 'reviews': + await userFavAndReviewHandler("/my-reviews"); + showReviews(); + break; + default: + break; + } +} + +// Initially load the content for the active tab +const initialActiveTab = document.querySelector('.active-tab'); +const initialTabName = initialActiveTab.getAttribute('data-tab'); +loadTabContent(initialTabName); \ No newline at end of file diff --git a/src/App.php b/src/App.php index a01ca1ea32d4d546bfc5fa331ab60d4dd0c5996e..d59e287c33e1d040c0cf1632665c100420e2899d 100644 --- a/src/App.php +++ b/src/App.php @@ -43,6 +43,8 @@ class App $this->router->addRoute('/add-film', CreateFilmController::class); $this->router->addRoute('/update-film', UpdateFilmController::class); $this->router->addRoute('/profile', ProfileController::class); + $this->router->addRoute('/my-favorites', ProfileController::class); + $this->router->addRoute('/my-reviews', ProfileController::class); $this->router->addRoute('/user-dashboard', UserDashboardController::class); } } diff --git a/src/controllers/ProfileController.php b/src/controllers/ProfileController.php index 5235e9d0453310a6e7b04cbda0309dac70336780..c2269831ca93f141e2869f9b0a366f485c5229b9 100644 --- a/src/controllers/ProfileController.php +++ b/src/controllers/ProfileController.php @@ -3,26 +3,76 @@ namespace app\controllers; use app\base\BaseController; +use app\controllers\utils\response; use app\exceptions\BadRequestException; +use app\models\FilmModel; use app\models\UserModel; use app\Request; +use app\services\FavoriteService; +use app\services\FilmService; +use app\services\ReviewService; use app\services\UserService; use Exception; class ProfileController extends BaseController { + protected $favoriteService; + protected $reviewService; + protected $filmService; public function __construct() { parent::__construct(UserService::getInstance()); + $this->favoriteService = FavoriteService::getInstance(); + $this->reviewService = ReviewService::getInstance(); + $this->filmService = FilmService::getInstance(); } protected function get($urlParams) { - - $user = $this->service->getById($_SESSION['user_id']); - $urlParams['email'] = $user->email; - $urlParams['username'] = $user->username; - parent::render($urlParams, "profile", "layouts/base"); + try + { + $uri = Request::getURL(); + $data = []; + if ($uri == "/profile") + { + $user = $this->service->getById($_SESSION['user_id']); + $data['email'] = $user->email; + $data['username'] = $user->username; + parent::render($data, "profile", "layouts/base"); + } + else if ($uri == "/my-favorites") + { + $favorites = $this->favoriteService->getUserFavoriteFilms($_SESSION['user_id']); + $films = []; + foreach ($favorites as $fav) { + $films[] = $this->filmService->getById($fav["film_id"]); + } + $filmsResp = []; + foreach ($films as $film) { + $filmsResp[] = $film->toResponse(); + } + $data['films'] = $filmsResp; + + response::send_json_response($data); + } + else if ($uri == "/my-reviews") + { + $user = $this->service->getById($_SESSION['user_id']); + $data['username'] = $user->username; + $reviews = $this->reviewService->getUserReviews($_SESSION['user_id']); + $reviewsResp = []; + foreach ($reviews as $review) { + $reviewsResp[] = $review->toResponse(); + } + $data["reviews"] = $reviewsResp; + response::send_json_response($data); + } + + } catch (Exception $e) { + $msg = $e->getMessage(); + $urlParams['errorMsg'] = $msg; + parent::render($urlParams, "profile", "layouts/base"); + } } protected function post($urlParams) diff --git a/src/models/FavoriteModel.php b/src/models/FavoriteModel.php new file mode 100644 index 0000000000000000000000000000000000000000..b8397e3f8c030c71584d4cf231166ff7b7ae2c0d --- /dev/null +++ b/src/models/FavoriteModel.php @@ -0,0 +1,27 @@ +<?php + +namespace app\models; +use app\base\BaseModel; + +class FavoriteModel extends BaseModel { + public $user_id; + public $film_id; + + public function __construct() { + $this-> _primary_key = ['user_id', 'film_id']; + return $this; + } + + public function constructFromArray($array) { + $this->user_id = $array['user_id']; + $this->film_id = $array['film_id']; + return $this; + } + + public function toResponse() { + return array( + 'user_id' => $this->user_id, + 'film_id' => $this->film_id, + ); + } +} \ No newline at end of file diff --git a/src/repositories/FavoriteRepository.php b/src/repositories/FavoriteRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..0a20e1609d3f3847bb1adf38a2a4361768a00184 --- /dev/null +++ b/src/repositories/FavoriteRepository.php @@ -0,0 +1,27 @@ +<?php + +namespace app\repositories; + +use app\base\BaseRepository; +use PDO; + +class FavoriteRepository extends BaseRepository { + protected static $instance; + protected $tableName = 'favorite'; + + private function __construct() { + parent::__construct(); + } + + public static function getInstance() { + if (!isset(self::$instance)) { + self::$instance = new static(); + } + return self::$instance; + } + + public function getUserFavorites($user_id) { + return $this->findAll(['user_id' => [$user_id, PDO::PARAM_INT]]); + } + +} \ No newline at end of file diff --git a/src/repositories/ReviewRepository.php b/src/repositories/ReviewRepository.php index e9615df4d31630e818972530e2ee1930c66bd942..20bde45f07e9603f81d479172919963096e5a501 100644 --- a/src/repositories/ReviewRepository.php +++ b/src/repositories/ReviewRepository.php @@ -45,6 +45,10 @@ class ReviewRepository extends BaseRepository return $this->findAll(['published_time' => [$published_time, PDO::PARAM_STR]], null, null, null, false); } + public function getByUserId($user_id) { + return $this->findAll(["user_id" => [$user_id, PDO::PARAM_INT]]); + } + public function deleteByUserFilmId($user_id, $film_id) { $review = $this->getById($user_id, $film_id); diff --git a/src/services/FavoriteService.php b/src/services/FavoriteService.php new file mode 100644 index 0000000000000000000000000000000000000000..687c0cf5dcf5565312f18e2c4c97e416c7e39d25 --- /dev/null +++ b/src/services/FavoriteService.php @@ -0,0 +1,35 @@ +<?php + +namespace app\services; + +use app\base\BaseService; +use app\controllers\FilmController; +use app\exceptions\BadRequestException; +use app\models\ReviewModel; +use app\repositories\FavoriteRepository; +use app\repositories\ReviewRepository; +use PDO; + +class FavoriteService extends BaseService { + protected static $instance; + protected $userRepository; + + private function __construct($repository) { + parent::__construct(); + $this->repository = $repository; + } + + public static function getInstance() { + if (!isset(self::$instance)) { + self::$instance = new static( + FavoriteRepository::getInstance() + ); + } + return self::$instance; + } + + public function getUserFavoriteFilms($user_id) { + return $this->repository->getUserFavorites($user_id); + } + +} \ No newline at end of file diff --git a/src/services/ReviewService.php b/src/services/ReviewService.php index fcfb48fda9f66fcda3cc9af246a5a20fc00de019..16708977be91ca0992d459992e1df63e35248c21 100644 --- a/src/services/ReviewService.php +++ b/src/services/ReviewService.php @@ -114,4 +114,16 @@ class ReviewService extends BaseService $arrParams['published_time'] = PDO::PARAM_STR; $this->repository->update($review, $arrParams); } + public function getUserReviews($user_id) { + $reviews = []; + $response = $this->repository->getByUserId($user_id); + if ($response) { + $review = new ReviewModel(); + $review->constructFromArray($response); + $reviews[] = $review; + } + return $reviews; + } + + } diff --git a/views/layouts/base.php b/views/layouts/base.php index 4142f2a1423fb59da51da1a9faf54bfbf9dfb035..45f7aee02bebb42d5e0353561e91950440ff4533 100644 --- a/views/layouts/base.php +++ b/views/layouts/base.php @@ -9,9 +9,10 @@ <link rel='stylesheet' href='/public/css/home.css'> <link rel='stylesheet' href='/public/css/styles.css'> <link rel='stylesheet' href='/public/css/filmList.css'> - <link rel='stylesheet' href='/public/css/filmDetails.css'> <link rel='stylesheet' href='/public/css/film-detail.css'> <link rel='stylesheet' href='/public/css/styles.css'> + <link rel='stylesheet' href='/public/css/film-detail.css'> + <link rel='stylesheet' href='/public/css/profile.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=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"> diff --git a/views/profile.php b/views/profile.php index 2c6f7a52982973495ebac39c40a496e7027ef2ea..cc730361d208cdda76f4a7bef4e3f50b396238ba 100644 --- a/views/profile.php +++ b/views/profile.php @@ -1,31 +1,54 @@ -<div class="form-container"> - <h2 class="header-title">Profile</h2> - <p class="error-msg"><?php if (isset($errorMsg)) { - echo "$errorMsg"; - } ?></p> - <form class="form" method="post" enctype="multipart/form-data"> - <div class="form-group"> - <label for="email">Email</label> - <br> - <input class="input" type="text" id="email" name="email" value="<?= $email ?>" required> +<div> + <div class="tab-container"> + <a href="/profile" class="tab active-tab" id="profile-tab" data-tab="profile">Profile</a> + <a href="/my-favorites" class="tab" id="favorites-tab" data-tab="favorites">My Favorites</a> + <a href="/my-reviews" class="tab" id="reviews-tab" data-tab="reviews">My Reviews</a> </div> - <div class="form-group"> - <label for="username">Username</label> - <br> - <input class="input" type="text" id="username" name="username" value="<?= $username ?>" required> + + <div id="profile-container"> + <div class="form-container"> + <h2 class="header-title">Profile</h2> + <p class="error-msg"><?php if (isset($errorMsg)) { + echo "$errorMsg"; + } ?></p> + <form class="form" method="post" enctype="multipart/form-data"> + <div class="form-group"> + <label for="email">Email</label> + <br> + <input class="input" type="text" id="email" name="email" value="<?= $email ?>" required> + </div> + <div class="form-group"> + <label for="username">Username</label> + <br> + <input class="input" type="text" id="username" name="username" value="<?= $username ?>" required> + </div> + <div class="form-group"> + <label for="password">Password</label> + <br> + <input class="input" type="password" id="password" name="password"> + </div> + <div class="form-group"> + <label for="confirm-password">Confirm Password</label> + <br> + <input class="input" type="password" id="confirm-password" name="confirm-password"> + </div> + <div class="form-group"> + <button class="button" ctype="submit">Update</button> + </div> + </form> + </div> </div> - <div class="form-group"> - <label for="password">Password</label> - <br> - <input class="input" type="password" id="password" name="password"> + + <div class="favorites-container"> + <div class="film-card-container" id="favorites-container"> </div> </div> - <div class="form-group"> - <label for="confirm-password">Confirm Password</label> - <br> - <input class="input" type="password" id="confirm-password" name="confirm-password"> + + <div class="reviews-container"> + <div class="review-card-container" id="reviews-container"> </div> </div> - <div class="form-group"> - <button class="button" ctype="submit">Update</button> - </div> - </form> -</div> \ No newline at end of file + + <script defer src="/public/js/httpClient.js"></script> + <script defer src="/public/js/utils.js"></script> + <script defer src="/public/js/profile.js"></script> +</div> +