diff --git a/src/public/css/global.css b/src/public/css/global.css index 3587c8b678006575469d9a200fe00c50f48ca85d..d358c67392517cb9609de03ae4b61e13da149238 100644 --- a/src/public/css/global.css +++ b/src/public/css/global.css @@ -800,7 +800,7 @@ button:hover { height: 100vh; justify-content: center; align-items: center; - background: rgba(15, 23, 42, 0.20); + background-color: rgba(255, 255, 255, .5); } .dialog__content { @@ -815,6 +815,7 @@ button:hover { border-radius: 10px; background: #FFF; margin: 6px; + border: 1px solid rgba(0, 0, 0, .1); } .dialog__content p, @@ -953,6 +954,10 @@ textarea:focus { fill: var(--accent-600); } +.icon-heart[data-type="unfilled"] { + fill: transparent; +} + .icon-bookmark:hover { fill: var(--accent-600); transition: all; @@ -964,6 +969,10 @@ textarea:focus { fill: var(--accent-600); } +.icon-bookmark[data-type="unfilled"] { + fill: transparent; +} + /* Modal */ .modal { diff --git a/src/public/css/watchlistCreate.css b/src/public/css/watchlistCreate.css index 03c12711d09cc3847fd4d1ebd98e09d151afaa5c..054e8ff15aecad03f8f36fbd7617f166a781f242 100644 --- a/src/public/css/watchlistCreate.css +++ b/src/public/css/watchlistCreate.css @@ -3,6 +3,7 @@ flex-direction: column; gap: 4rem; align-items: flex-start; + margin-bottom: 8rem; } .container__form { @@ -49,12 +50,17 @@ } .actions { + position: fixed; + bottom: 0; + left: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1rem; width: 100%; + background-color: white; + padding: 2rem 3rem; } .btn__add-item { @@ -96,6 +102,8 @@ .actions { position: sticky; top: 8rem; + left: auto; + bottom: auto; max-width: 20rem; } } \ No newline at end of file diff --git a/src/public/js/catalog/createUpdate.js b/src/public/js/catalog/createUpdate.js new file mode 100644 index 0000000000000000000000000000000000000000..a9986031bc7d281aea32489c966bcb7491205dcc --- /dev/null +++ b/src/public/js/catalog/createUpdate.js @@ -0,0 +1,185 @@ +const CATEGORY = ["ANIME", "DRAMA"]; + +function validateInput(formData, update = false) { + if (!formData.get("title") || formData.get("title").trim() === "") { + return { + valid: false, + message: "Title is required.", + }; + } + if (formData.get("title").length > 40) { + return { + valid: false, + message: "Title is too long. Maximum 40 chars.", + }; + } + if (formData.get("description") && formData.get("description").length > 255) { + return { + valid: false, + message: "Description is too long. Maximum 255 chars.", + }; + } + if ( + !formData.get("category") || + !CATEGORY.includes(formData.get("category").trim()) + ) { + return { + valid: false, + message: "Category is invalid.", + }; + } + + if (!update && !formData.get("poster").name) { + return { + valid: false, + message: "Poster is required.", + }; + } + return { + valid: true, + }; +} + +function createCatalog(form) { + const apiUrl = `/api/catalog/create`; + const formData = new FormData(form); + + const validate = validateInput(formData); + + if (!validate.valid) { + showToast("Error", validate.message, "error"); + return; + } + + const xhttp = new XMLHttpRequest(); + xhttp.open("POST", apiUrl, true); + + xhttp.onreadystatechange = function () { + if (xhttp.readyState === 4) { + const response = JSON.parse(xhttp.responseText); + if (xhttp.status === 200 && response.status === 200) { + showToast( + "Success", + `Catalog ${response.data.title} created`, + "success" + ); + setTimeout(() => { + window.location.href = `/catalog/${response.data.uuid}`; + }, [1000]); + } else { + try { + showToast("Error", response.message); + } catch (e) { + showToast("Error", "Something went wrong", "error"); + } + } + } + }; + + xhttp.send(formData); +} + +function updateCatalog(form) { + const urlParts = window.location.pathname.split("/"); + const uuidIndex = urlParts.indexOf("catalog") + 1; + const uuid = urlParts[uuidIndex]; + const apiUrl = `/api/catalog/${uuid}/update`; + const formData = new FormData(form); + + const validate = validateInput(formData, true); + + if (!validate.valid) { + showToast("Error", validate.message, "error"); + return; + } + + const xhttp = new XMLHttpRequest(); + xhttp.open("POST", apiUrl, true); + + xhttp.onreadystatechange = function () { + if (xhttp.readyState === 4) { + const response = JSON.parse(xhttp.responseText); + if (xhttp.status === 200 && response.status === 200) { + showToast("Success", "Catalog updated", "success"); + setTimeout(() => { + window.location.href = `/catalog/${uuid}`; + }, [1000]); + } else { + try { + showToast("Error", response.message); + } catch (e) { + showToast("Error", "Something went wrong", "error"); + } + } + } + }; + + xhttp.send(formData); +} + +const form = document.querySelector("form#catalog-create-update"); + +form.addEventListener("submit", function (event) { + event.preventDefault(); + + const currentUrl = window.location.href; + const url = new URL(currentUrl); + const urlPathname = url.pathname.split("/"); + const action = urlPathname[urlPathname.length - 1].toLowerCase(); + + if (action === "create") { + dialog( + "Create Catalog", + `Are you sure you want to create this catalog?`, + "create", + "create", + "Confirm", + () => { + createCatalog(form); + } + ); + } else if (action === "edit") { + dialog( + "Update Catalog", + `Are you sure you want to update this catalog?`, + "update", + "update", + "Confirm", + () => { + updateCatalog(form); + } + ); + } +}); + +posterImg = document.querySelector("img#poster"); +if (posterImg) { + posterInput = document.querySelector("input#posterField"); + posterInput.addEventListener("change", function (event) { + if (this.files[0]) { + var reader = new FileReader(); + + reader.readAsDataURL(this.files[0]); + + reader.onloadend = function () { + posterImg.src = reader.result; + }; + } + }); +} + +trailerVideo = document.querySelector("video#trailer"); +if (trailerVideo) { + trailerInput = document.querySelector("input#trailerField"); + trailerInput.addEventListener("change", function (event) { + if (this.files[0]) { + var reader = new FileReader(); + + reader.readAsDataURL(this.files[0]); + + reader.onloadend = function () { + trailerVideo.src = reader.result; + }; + } + }); +} diff --git a/src/public/js/deleteCatalog.js b/src/public/js/catalog/delete.js similarity index 96% rename from src/public/js/deleteCatalog.js rename to src/public/js/catalog/delete.js index 6671a08ee4aea6f48cd77ed74e65a1308538f9e2..de2c8056536e030b53137e4ef05326d313a0b305 100644 --- a/src/public/js/deleteCatalog.js +++ b/src/public/js/catalog/delete.js @@ -8,7 +8,7 @@ function deleteCatalog(uuid, title) { if (xhttp.status === 200) { showToast("Success", `Catalog ${title} deleted`, "success"); setTimeout(() => { - window.location.reload(); + window.location.href = "/catalog"; }, 1000); } else { try { diff --git a/src/public/js/editCatalog.js b/src/public/js/editCatalog.js deleted file mode 100644 index 8926631df87497c5cdf576b2c4e31f9e377123e5..0000000000000000000000000000000000000000 --- a/src/public/js/editCatalog.js +++ /dev/null @@ -1,47 +0,0 @@ -function updateCatalog(form) { - const urlParts = window.location.pathname.split("/"); - const uuidIndex = urlParts.indexOf("catalog") + 1; - const uuid = urlParts[uuidIndex]; - const apiUrl = `/api/catalog/${uuid}/update`; - const formData = new FormData(form); - - const xhttp = new XMLHttpRequest(); - xhttp.open("POST", apiUrl, true); - - xhttp.onreadystatechange = function () { - if (xhttp.readyState === 4) { - if (xhttp.status === 200) { - showToast("Success", "Catalog updated", "success"); - setTimeout(() => { - window.location.href = `/catalog/${uuid}`; - }, [1000]); - } else { - try { - const response = JSON.parse(xhttp.responseText); - showToast("Error", response.message); - } catch (e) { - showToast("Error", "Something went wrong", "error"); - } - } - } - }; - - xhttp.send(formData); -} - -const form = document.getElementById("catalog-edit-form"); - -form.addEventListener("submit", function (event) { - event.preventDefault(); - - dialog( - "Update Catalog", - `Are you sure you want to update this catalog?`, - "update", - "update", - "Confirm", - () => { - updateCatalog(form); - } - ); -}); diff --git a/src/public/js/global.js b/src/public/js/global.js index dfbb814ff0122f0d1f1efd62a876108a3ca7185f..bce2ff47c26a85586d54d343f0d746f8b9a0f71d 100644 --- a/src/public/js/global.js +++ b/src/public/js/global.js @@ -1,29 +1,50 @@ function showToast(title, message, type = "error") { - const toast = document.getElementById("toast"); - if (toast) { - toast.classList.remove("hidden"); - toast.setAttribute("data-type", type); - h3 = toast.querySelector("h3"); - h3.textContent = title; - p = toast.querySelector("p"); - p.textContent = message; - } + const oldToast = document.querySelector("#toast"); + if (oldToast) { + oldToast.remove(); + } + const toast = document.createElement("div"); + toast.id = "toast"; + toast.classList.add("toast"); + toast.setAttribute("data-type", type); + toast.innerHTML = ` + <div> + <h3> + ${title} + </h3> + <p> + ${message} + </p> +</div> + <button id="close" class="btn-ghost"> + <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none"> + <path d="M1 13L7.00001 7.00002M7.00001 7.00002L13 1M7.00001 7.00002L1 1M7.00001 7.00002L13 13" stroke="black" + stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> + </svg> + </button> + `; + const body = document.querySelector("body"); + body.appendChild(toast); + const toastClose = toast.querySelector("button"); + toastClose.addEventListener("click", () => { + toast.remove(); + }); } function dialog( - title, - message, - dialogId, - actionId, - actionButtonText, - onaction + title, + message, + dialogId, + actionId, + actionButtonText, + onaction ) { - const body = document.querySelector("body"); + const body = document.querySelector("body"); - const dialog = document.createElement("div"); - dialog.classList.add("dialog"); - dialog.id = `dialog-${dialogId}`; - dialog.innerHTML = ` + const dialog = document.createElement("div"); + dialog.classList.add("dialog"); + dialog.id = `dialog-${dialogId}`; + dialog.innerHTML = ` <div class="dialog__content"> <h2> ${title} @@ -42,129 +63,120 @@ function dialog( </div> `; - body.appendChild(dialog); + body.appendChild(dialog); - const cancelButton = dialog.querySelector("#cancel"); - cancelButton.addEventListener("click", () => { - dialog.remove(); - }); + const cancelButton = dialog.querySelector("#cancel"); + cancelButton.addEventListener("click", () => { + dialog.remove(); + }); - const actionButton = dialog.querySelector(`#${actionId}`); - actionButton.addEventListener("click", () => { - dialog.remove(); - onaction(); - }); + const actionButton = dialog.querySelector(`#${actionId}`); + actionButton.addEventListener("click", () => { + dialog.remove(); + onaction(); + }); } // Lik and Save Watchlist function like() { - const btnLikes = document.querySelectorAll(".btn__like"); - btnLikes.forEach(btn => { - if (btn.dataset.id) { - let liked = btn.dataset.liked; - btn.addEventListener("click", () => { - const iconLike = btn.querySelector('.icon-heart'); - const likeCountEl = document.querySelector(`span[data-id='${btn.dataset.id}']`); - if (!liked) { - likeCountEl.innerHTML = `${parseInt(likeCountEl.textContent) + 1}`; - iconLike.dataset.type = "filled"; - } else { - likeCountEl.innerHTML = `${parseInt(likeCountEl.textContent) - 1}`; - iconLike.dataset.type = "unfilled"; - } - liked = !liked; - - let data = { - "watchlistUUID": btn.dataset.id - } - data = JSON.stringify(data); - - const xhttp = new XMLHttpRequest(); - xhttp.open("POST", "/api/watchlist/like", true); - xhttp.setRequestHeader("Content-Type", "application/json"); - - xhttp.onreadystatechange = function () { - if (this.readyState === 4) { - console.log(JSON.parse(this.response)); - } - } - - xhttp.send(data); - }) + const btnLikes = document.querySelectorAll(".btn__like"); + btnLikes.forEach((btn) => { + if (btn.dataset.id) { + let liked = btn.dataset.liked; + btn.addEventListener("click", () => { + const iconLike = btn.querySelector(".icon-heart"); + const likeCountEl = document.querySelector( + `span[data-id='${btn.dataset.id}']` + ); + if (!liked) { + likeCountEl.innerHTML = `${parseInt(likeCountEl.textContent) + 1}`; + iconLike.dataset.type = "filled"; + } else { + likeCountEl.innerHTML = `${parseInt(likeCountEl.textContent) - 1}`; + iconLike.dataset.type = "unfilled"; } - }) + liked = !liked; + + let data = { + watchlistUUID: btn.dataset.id, + }; + data = JSON.stringify(data); + + const xhttp = new XMLHttpRequest(); + xhttp.open("POST", "/api/watchlist/like", true); + xhttp.setRequestHeader("Content-Type", "application/json"); + + xhttp.onreadystatechange = function () { + if (this.readyState === 4) { + } + }; + + xhttp.send(data); + }); + } + }); } function save() { - const btnSaves = document.querySelectorAll(".btn__save"); - btnSaves.forEach(btn => { - if (btn.dataset.id) { - let saved = btn.dataset.saved; - btn.addEventListener("click", () => { - const iconSaved = btn.querySelector(".icon-bookmark"); - if (!saved) { - iconSaved.dataset.type = "filled"; - } else { - iconSaved.dataset.type = "unfilled"; - } - saved = !saved; - - let data = { - "watchlistUUID": btn.dataset.id - } - data = JSON.stringify(data); - - const xhttp = new XMLHttpRequest(); - xhttp.open("POST", "/api/watchlist/save", true); - xhttp.setRequestHeader("Content-Type", "application/json"); - - xhttp.onreadystatechange = function () { - if (this.readyState === 4) { - console.log(JSON.parse(this.response)); - } - } - - xhttp.send(data); - }) + const btnSaves = document.querySelectorAll(".btn__save"); + btnSaves.forEach((btn) => { + if (btn.dataset.id) { + let saved = btn.dataset.saved; + btn.addEventListener("click", () => { + const iconSaved = btn.querySelector(".icon-bookmark"); + if (!saved) { + iconSaved.dataset.type = "filled"; + } else { + iconSaved.dataset.type = "unfilled"; } - }) -} + saved = !saved; -toastClose = document.querySelector("#toast button"); - -if (toastClose) { - toastClose.addEventListener("click", () => { - const toast = document.querySelector("#toast"); - toast.classList.add("hidden"); - }); -} + let data = { + watchlistUUID: btn.dataset.id, + }; + data = JSON.stringify(data); -logoutBtn = document.querySelector("button#logout"); -if (logoutBtn) { - logoutBtn.addEventListener("click", () => { const xhttp = new XMLHttpRequest(); - xhttp.open("POST", "/api/auth/logout", true); + xhttp.open("POST", "/api/watchlist/save", true); xhttp.setRequestHeader("Content-Type", "application/json"); xhttp.onreadystatechange = function () { - if (this.readyState === 4) { - if (xhttp.status === 200) { - showToast("Success", "Logout success", "success"); - setTimeout(() => { - window.location.href = `/`; - }, [1000]); - } else { - try { - const response = JSON.parse(xhttp.responseText); - showToast("Error", response.message); - } catch (e) { - showToast("Error", "Something went wrong", "error"); - } - } - } + if (this.readyState === 4) { + } }; - xhttp.send(); - }); + xhttp.send(data); + }); + } + }); +} + +logoutBtn = document.querySelector("button#logout"); +if (logoutBtn) { + logoutBtn.addEventListener("click", () => { + const xhttp = new XMLHttpRequest(); + xhttp.open("POST", "/api/auth/logout", true); + xhttp.setRequestHeader("Content-Type", "application/json"); + + xhttp.onreadystatechange = function () { + if (this.readyState === 4) { + if (xhttp.status === 200) { + showToast("Success", "Logout success", "success"); + setTimeout(() => { + window.location.href = `/`; + }, [1000]); + } else { + try { + const response = JSON.parse(xhttp.responseText); + showToast("Error", response.message); + } catch (e) { + showToast("Error", "Something went wrong", "error"); + } + } + } + }; + + xhttp.send(); + }); } diff --git a/src/public/js/profile.js b/src/public/js/profile.js index 23e1e12ed3cc5541ca97d6f1a5d8f59fe256b11d..f1c3ebf7a57729d5e34ffcd01a336e912a7c224f 100644 --- a/src/public/js/profile.js +++ b/src/public/js/profile.js @@ -42,7 +42,7 @@ if (deleteTriggerButton) { function updateAccount(form) { const xhttp = new XMLHttpRequest(); - xhttp.open("POST", `/api/auth/update`, true); + xhttp.open("PUT", `/api/auth/update`, true); xhttp.setRequestHeader("Content-Type", "application/json"); xhttp.onreadystatechange = function () { diff --git a/src/public/js/profile/bookmark.js b/src/public/js/profile/bookmark.js new file mode 100644 index 0000000000000000000000000000000000000000..cce092b5bcd048e8dfe1c8bb3be08b2e665f9fdd --- /dev/null +++ b/src/public/js/profile/bookmark.js @@ -0,0 +1,2 @@ +like(); +save(); diff --git a/src/public/js/profile/watchlist.js b/src/public/js/profile/watchlist.js new file mode 100644 index 0000000000000000000000000000000000000000..d8733fcc8190c1d625dd9c94cd829549498e900f --- /dev/null +++ b/src/public/js/profile/watchlist.js @@ -0,0 +1 @@ +like(); diff --git a/src/seed/seed.sql b/src/seed/seed.sql index 0343510eaf16596677d34f239b2212e7b83edbfa..4e6257e61d30c560c64b1348d3fd49ef03144a4a 100644 --- a/src/seed/seed.sql +++ b/src/seed/seed.sql @@ -1,59 +1,19 @@ -INSERT INTO users (uuid, name, password, email, role, created_at, updated_at) -VALUES ('1a2b3c', 'John Doe', 'password123', 'john@example.com', 'BASIC', NOW(), NOW()), - ('4d5e6f', 'Jane Smith', 'securepwd', 'jane@example.com', 'ADMIN', NOW(), NOW()), - ('7g8h9i', 'Alice Johnson', 'pass123', 'alice@example.com', 'BASIC', NOW(), NOW()); - -INSERT INTO catalogs (uuid, title, description, poster, trailer, category, created_at, updated_at) -VALUES ('abc123', 'Anime Show 1', 'Description 1', 'poster1.jpg', 'trailer1.mp4', 'ANIME', NOW(), NOW()), - ('def456', 'Drama Series 1', 'Description 2', 'poster2.jpg', 'trailer2.mp4', 'DRAMA', NOW(), NOW()), - ('ghi789', 'Mixed Content 1', 'Description 3', 'poster3.jpg', 'trailer3.mp4', 'ANIME', NOW(), NOW()); - -INSERT INTO watchlists (uuid, title, description, category, user_id, like_count, visibility, created_at, updated_at) -VALUES ('wxyz123', 'My Watchlist 1', 'Watchlist description 1', 'MIXED', 1, 0, 'PRIVATE', NOW(), NOW()), - ('lmnop45', 'Favorites 1', 'Favorites description 1', 'ANIME', 2, 0, 'PUBLIC', NOW(), NOW()), - ('qrstuv67', 'To Watch 1', 'To Watch description 1', 'DRAMA', 3, 0, 'PRIVATE', NOW(), NOW()); - -INSERT INTO watchlist_catalog (watchlist_id, catalog_id) -VALUES (1, 1), - (1, 2), - (2, 3); - -INSERT INTO watchlist_like (user_id, watchlist_id) -VALUES (1, 2), - (2, 1), - (3, 3); - -INSERT INTO watchlist_save (user_id, watchlist_id) -VALUES (2, 1), - (3, 2), - (1, 3); - -INSERT INTO comments (uuid, content, user_id, created_at) -VALUES ('comment1', 'This is a comment.', 1, NOW()), - ('comment2', 'Another comment here.', 2, NOW()), - ('comment3', 'A third comment.', 3, NOW()); - -INSERT INTO watchlist_comment (watchlist_id, comment_id) -VALUES (1, 1), - (2, 2), - (3, 3); - - --- QUERY PLAN GET WATCHLISTS - --- 1. PAGINATE WATCHLISTS +INSERT INTO catalogs (uuid, title, description, poster, trailer, category) +SELECT + md5(random()::text || clock_timestamp()::text)::uuid, + 'Anime Title ' || num, + 'Anime Description ' || num, + '0f57456ef87ea61a.webp', + '86fa25a6dad7fcc7.mp4', + 'ANIME' +FROM generate_series(1, 100) AS num; + +INSERT INTO catalogs (uuid, title, description, poster, trailer, category) SELECT -FROM - users AS u JOIN - (SELECT - id, - uuid, - title, - description, - user_id, - updated_at, - like_count - FROM - watchlist - LIMIT 20 - OFFSET 20) as w ON u.id = w.user_id + md5(random()::text || clock_timestamp()::text)::uuid, + 'Drama Title ' || num, + 'Drama Description ' || num, + 'a9f6e15daf0eca96.webp', + '86fa25a6dad7fcc7.mp4', + 'DRAMA' +FROM generate_series(1, 100) AS num; diff --git a/src/server/app/App/View.php b/src/server/app/App/View.php index 4278869ca138a1d8064a429c8838ffcb2f749ad0..41c386e2540f66ed0cf3fde5b5f43084d4efc38e 100644 --- a/src/server/app/App/View.php +++ b/src/server/app/App/View.php @@ -8,7 +8,6 @@ class View { $user = $sessionService ? $sessionService->current() : null; - require __DIR__ . '/../View/components/toast.php'; require __DIR__ . '/../View/components/header.php'; if (!isset($_SERVER['PATH_INFO']) || ($_SERVER['PATH_INFO'] != '/signin' && $_SERVER['PATH_INFO'] != '/signup')) { require __DIR__ . '/../View/components/navbar.php'; diff --git a/src/server/app/Controller/BookmarkController.php b/src/server/app/Controller/BookmarkController.php index 4a131ebc40d4aaf3b6a353dfa141f90ef765ee89..9b2c93f0bb57ae1b6c92386ef6377bdeb5659787 100644 --- a/src/server/app/Controller/BookmarkController.php +++ b/src/server/app/Controller/BookmarkController.php @@ -63,6 +63,9 @@ class BookmarkController View::render('profile/bookmark', [ 'title' => 'Bookmark', + 'js' => [ + '/js/profile/bookmark.js' + ], 'data' => [ 'bookmarks' => $result, 'userUUID' => $user ? $user->uuid : null, diff --git a/src/server/app/Controller/CatalogController.php b/src/server/app/Controller/CatalogController.php index c77b6a9f786b44bc56bda12287b79d1dc2c1766d..fca0b5899ec27f13fd25175a97ecb220540b3bbd 100644 --- a/src/server/app/Controller/CatalogController.php +++ b/src/server/app/Controller/CatalogController.php @@ -45,7 +45,7 @@ class CatalogController '/css/catalog.css', ], 'js' => [ - '/js/deleteCatalog.js' + '/js/catalog/delete.js' ], 'data' => [ 'catalogs' => $this->catalogService->findAll($page, $category), @@ -62,6 +62,9 @@ class CatalogController 'styles' => [ '/css/catalog-form.css', ], + 'js' => [ + '/js/catalog/createUpdate.js' + ], 'type' => 'create' ], $this->sessionService); } @@ -74,13 +77,13 @@ class CatalogController View::redirect('/404'); } - View::render('catalog/edit', [ + View::render('catalog/form', [ 'title' => 'Edit Catalog', 'styles' => [ '/css/catalog-form.css', ], 'js' => [ - '/js/editCatalog.js' + '/js/catalog/createUpdate.js' ], 'type' => 'edit', 'data' => $catalog->toArray() @@ -103,7 +106,7 @@ class CatalogController '/css/catalog-detail.css', ], 'js' => [ - '/js/deleteCatalog.js' + '/js/catalog/delete.js' ], 'data' => [ 'item' => $catalog->toArray(), @@ -131,22 +134,34 @@ class CatalogController } try { - $this->catalogService->create($request); - View::redirect('/catalog'); - } catch (ValidationException $exception) { - View::render('catalog/form', [ - 'title' => 'Add Catalog', - 'error' => $exception->getMessage(), - 'styles' => [ - '/css/catalog-form.css', - ], - 'type' => 'create', - 'data' => [ - 'title' => $request->title, - 'description' => $request->description, - 'category' => $request->category, + $response = $this->catalogService->create($request); + http_response_code(200); + $response = [ + "status" => 200, + "message" => "Successfully created catalog", + "data" => [ + "uuid" => $response->catalog->uuid, + "title" => $response->catalog->title, ] - ], $this->sessionService); + ]; + + echo json_encode($response); + } catch (ValidationException $exception) { + http_response_code(400); + $response = [ + "status" => 400, + "message" => $exception->getMessage(), + ]; + + echo json_encode($response); + } catch (\Exception $exception) { + http_response_code(500); + $response = [ + "status" => 500, + "message" => "Something went wrong.", + ]; + + echo json_encode($response); } } @@ -165,6 +180,7 @@ class CatalogController $uuid = $item->uuid; $description = $item->description; $category = $item->category; + $page = $catalogs->catalogs['page']; require __DIR__ . '/../View/components/modal/watchlistAddSearchItem.php'; } } diff --git a/src/server/app/Controller/WatchlistController.php b/src/server/app/Controller/WatchlistController.php index b1ae74b9011581b21467454217b6920c1ef79334..adcee4eb8996fc3c7b2640487c89f0c34642e4cc 100644 --- a/src/server/app/Controller/WatchlistController.php +++ b/src/server/app/Controller/WatchlistController.php @@ -305,6 +305,9 @@ class WatchlistController 'styles' => [ '/css/watchlist-self.css', ], + 'js' => [ + '/js/profile/watchlist.js', + ], 'data' => [ 'visibility' => strtolower($_GET['visibility'] ?? 'all'), 'watchlists' => $result, @@ -335,7 +338,7 @@ class WatchlistController try { $this->watchlistService->like($watchlistLikeRequest); - + http_response_code(200); $response = [ "status" => 200, "message" => "Success", @@ -368,6 +371,7 @@ class WatchlistController try { $this->watchlistService->bookmark($watchlistSaveRequest); + http_response_code(200); $response = [ "status" => 200, diff --git a/src/server/app/Repository/WatchlistSaveRepository.php b/src/server/app/Repository/WatchlistSaveRepository.php index cc9778ade84217ec89b3a53ab25c03da41471059..04543911c44f96e21b2edd8a5679f3e875c59935 100644 --- a/src/server/app/Repository/WatchlistSaveRepository.php +++ b/src/server/app/Repository/WatchlistSaveRepository.php @@ -41,6 +41,7 @@ class WatchlistSaveRepository extends Repository JOIN watchlist_save wv ON w.id = wv.watchlist_id WHERE wv.user_id = :user_id + AND w.visibility = 'PUBLIC' LIMIT :limit OFFSET :offset ) AS w diff --git a/src/server/app/Service/CatalogService.php b/src/server/app/Service/CatalogService.php index cc946570077ff8180b671de4511eea67f0983195..f340805663344e6e679335c39b987cb5feb592ff 100644 --- a/src/server/app/Service/CatalogService.php +++ b/src/server/app/Service/CatalogService.php @@ -108,10 +108,22 @@ class CatalogService throw new ValidationException("Title cannot be blank."); } + if (strlen($request->title) > 40) { + throw new ValidationException("Title cannot be more than 40 characters."); + } + + if (strlen($request->description) > 255) { + throw new ValidationException("Description cannot be more than 255 characters."); + } + if ($request->category == null || trim($request->category) == "") { throw new ValidationException("Category cannot be blank."); } + if ($request->category != "ANIME" && $request->category != "DRAMA") { + throw new ValidationException("Category must be either ANIME or DRAMA."); + } + if ($request->poster == null || $request->poster['error'] != UPLOAD_ERR_OK) { throw new ValidationException("Poster cannot be blank."); } @@ -171,9 +183,21 @@ class CatalogService throw new ValidationException("Title cannot be blank."); } + if (strlen($request->title) > 40) { + throw new ValidationException("Title cannot be more than 40 characters."); + } + + if (strlen($request->description) > 255) { + throw new ValidationException("Description cannot be more than 255 characters."); + } + if ($request->category == null || trim($request->category) == "") { throw new ValidationException("Category cannot be blank."); } + + if ($request->category != "ANIME" && $request->category != "DRAMA") { + throw new ValidationException("Category must be either ANIME or DRAMA."); + } } public function search(CatalogSearchRequest $catalogSearchRequest): CatalogSearchResponse diff --git a/src/server/app/View/catalog/form.php b/src/server/app/View/catalog/form.php index 20e49bd945603c37be56ec70c26c54dd29feef44..830b7ea5908b78c98b8aa21f80985f38b3c29c31 100644 --- a/src/server/app/View/catalog/form.php +++ b/src/server/app/View/catalog/form.php @@ -21,12 +21,7 @@ function alert($title, $message) <h2> <?= $model['title'] ?> </h2> - <?php if (isset($model['error'])): ?> - <?php alert('Failed to ' . $model['title'], $model['error']); ?> - <?php endif; ?> - <form action="/catalog/<?= $model['type'] === "create" ? "create" : $model['data']['uuid'] . "/edit" ?>" - method="POST" enctype="multipart/form-data"> - + <form id="catalog-create-update"> <div class="input-group"> <label class="input-required">Category</label> <?php selectCategory($model['data']['category'] ?? 'ANIME'); ?> @@ -48,22 +43,24 @@ function alert($title, $message) <div class="input-group"> <label for="posterField" class="input-required">Poster</label> <?php if (isset($model['data']['poster'])): ?> - <img class="poster" src="<?= '/assets/images/catalogs/posters/' . $model['data']['poster'] ?>" + <img id="poster" class="poster" src="<?= '/assets/images/catalogs/posters/' . $model['data']['poster'] ?>" alt="<?= 'Poster of ' . $model['data']['title'] ?>"> <input type="file" id="posterField" name="poster" accept="image/*"> <?php else: ?> - <?php endif; ?> - <?php if ($model['type'] === 'create'): ?> <input type="file" id="posterField" name="poster" accept="image/*" required> <?php endif; ?> </div> <div class="input-group"> + <?php if (isset($model['data']['trailer']) && $model['data']['trailer'] !== null): ?> + <video id="trailer" class="catalog-trailer" controls> + <source src="<?= '/assets/videos/catalogs/trailers/' . $model['data']['trailer'] ?>" type="video/mp4"> + </video> + <?php endif; ?> <label for="trailerField">Trailer</label> <input type="file" id="trailerField" name="trailer" accept="video/mp4"> </div> - - <button class="btn-bold" type="submit"> - Submit + <button id="save" class="btn-bold" type="submit"> + Save </button> </form> </main> \ No newline at end of file diff --git a/src/server/app/View/components/card/watchlistCard.php b/src/server/app/View/components/card/watchlistCard.php index 2ea3518bbb9cd92b8e075e2b394070408474bd8b..5c2784bf117973fc7caac669af99f1cd42e60163 100644 --- a/src/server/app/View/components/card/watchlistCard.php +++ b/src/server/app/View/components/card/watchlistCard.php @@ -28,8 +28,8 @@ if (!function_exists("likeAndSave")) { if (!isset($posters[3 - $i])): ?> <div> <img loading=<?= $loading ?? 'eager' ?> - src="<?= "/assets/images/catalogs/posters/" . (isset($posters[3 - $i]) ? $posters[3 - $i]["poster"] : "no-poster.webp") ?>" - alt="Anime or Drama Poster" class="poster"/> + src="<?= "/assets/images/catalogs/posters/" . (isset($posters[3 - $i]) ? $posters[3 - $i]["poster"] : "no-poster.webp") ?>" + alt="Anime or Drama Poster" class="poster" /> </div> <?php else: ?> <?php @@ -39,8 +39,8 @@ if (!function_exists("likeAndSave")) { ?> <a href="/catalog<?= isset($posters[3 - $i]) ? "/" . $posters[3 - $i]["catalog_uuid"] : "" ?>"> <img loading=<?= $loading ?? 'eager' ?> - src="<?= "/assets/images/catalogs/posters/" . (isset($posters[3 - $i]) ? $posters[3 - $i]["poster"] : "no-poster.webp") ?>" - alt="Anime or Drama Poster" class="poster"/> + src="<?= "/assets/images/catalogs/posters/" . (isset($posters[3 - $i]) ? $posters[3 - $i]["poster"] : "no-poster.webp") ?>" + alt="Anime or Drama Poster" class="poster" /> </a> <?php endif; ?> <?php endfor; ?> @@ -50,8 +50,8 @@ if (!function_exists("likeAndSave")) { <?php if ($visibility === "PRIVATE"): ?> <svg xmlns="http://www.w3.org/2000/svg" width="12" height="14" viewBox="0 0 12 14" fill="none"> <path - d="M9 7H10.05C10.2985 7 10.5 7.20145 10.5 7.45V12.55C10.5 12.7985 10.2985 13 10.05 13H1.95C1.70147 13 1.5 12.7985 1.5 12.55V7.45C1.5 7.20145 1.70147 7 1.95 7H3M9 7V4C9 3 8.4 1 6 1C3.6 1 3 3 3 4V7M9 7H3" - stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> + d="M9 7H10.05C10.2985 7 10.5 7.20145 10.5 7.45V12.55C10.5 12.7985 10.2985 13 10.05 13H1.95C1.70147 13 1.5 12.7985 1.5 12.55V7.45C1.5 7.20145 1.70147 7 1.95 7H3M9 7V4C9 3 8.4 1 6 1C3.6 1 3 3 3 4V7M9 7H3" + stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> </svg> <?php endif; ?> <a href="/watchlist/<?= $uuid ?>"> @@ -66,7 +66,9 @@ if (!function_exists("likeAndSave")) { <?= $category ?> </span> <?php for ($i = 0; $i < min(3, count($item["tags"])); $i++): ?> - <span class="tag"><?= $item["tags"][$i]["name"] ?></span> + <span class="tag"> + <?= $item["tags"][$i]["name"] ?> + </span> <?php endfor; ?> <span class="subtitle">by <span class="author-name"> <?= $creator ?> @@ -92,15 +94,14 @@ if (!function_exists("likeAndSave")) { <?php if ($userUUID == ""): ?> <?php likeAndSave("btn__save", "bookmark"); ?> <?php else: ?> - <button aria-label="Save <?= $title ?>" type="button" class="btn-ghost btn__save" - data-id="<?= $uuid ?>" - data-saved="<?= $saved ?>"> + <button aria-label="Save <?= $title ?>" type="button" class="btn-ghost btn__save" data-id="<?= $uuid ?>" + data-saved="<?= $saved ?>"> <?php if (isset($saved)) { $type = $saved ? "filled" : "unfilled"; } require PUBLIC_PATH . 'assets/icons/bookmark.php' - ?> + ?> </button> <?php endif; ?> <?php endif; ?> @@ -110,7 +111,7 @@ if (!function_exists("likeAndSave")) { <?php likeAndSave("btn__like", "love"); ?> <?php else: ?> <button aria-label="Love <?= $title ?>" type="button" class="btn-ghost btn__like" data-id="<?= $uuid ?>" - data-liked="<?= $loved ?>"> + data-liked="<?= $loved ?>"> <?php if (isset($loved)) { $type = $loved ? "filled" : "unfilled"; @@ -118,7 +119,9 @@ if (!function_exists("likeAndSave")) { require PUBLIC_PATH . 'assets/icons/love.php' ?> </button> <?php endif; ?> - <span data-id="<?= $uuid ?>"><?= $loveCount ?></span> + <span data-id="<?= $uuid ?>"> + <?= $loveCount ?> + </span> </div> </div> </div> \ No newline at end of file diff --git a/src/server/app/View/components/toast.php b/src/server/app/View/components/toast.php index 03e0577a8e9e2cc3574585020d4d4949f1e024dc..773d59aca1320702f7b2dd247a5644f078e10394 100644 --- a/src/server/app/View/components/toast.php +++ b/src/server/app/View/components/toast.php @@ -1,7 +1,11 @@ -<div id="toast" class="toast hidden" data-type="error"> +<div id="toast" class="toast hidden" data-type="<?= $type ?? "error" ?>"> <div> - <h3></h3> - <p></p> + <h3> + <?= $title ?? "" ?> + </h3> + <p> + <?= $message ?? "" ?> + </p> </div> <button id="close" class="btn-ghost"> <?php require PUBLIC_PATH . "assets/icons/cancel.php" ?> diff --git a/src/server/app/View/profile/bookmark.php b/src/server/app/View/profile/bookmark.php index 4915cc04da0cbc1de03d50c883c85d9aa8855996..f7fa0ffbfd61c00096eebaab47d7b9958b683893 100644 --- a/src/server/app/View/profile/bookmark.php +++ b/src/server/app/View/profile/bookmark.php @@ -1,6 +1,6 @@ <?php -function watchlistCard(array $item, string $userUUID, bool $saved = true, bool $loved = false, string $loading = "eager") +function watchlistCard(array $item, string $userUUID, string $loading = "eager") { $uuid = $item["watchlist_uuid"]; $posters = $item["posters"]; @@ -12,8 +12,9 @@ function watchlistCard(array $item, string $userUUID, bool $saved = true, bool $ $description = $item["description"]; $itemCount = $item["item_count"]; $loveCount = $item["like_count"]; + $loved = $item["liked"]; $self = ($userUUID == $item["creator_uuid"]); - + $saved = true; require __DIR__ . '/../components/card/watchlistCard.php'; } @@ -39,7 +40,7 @@ function pagination(int $currentPage, int $totalPage) <?php endif; ?> <section class="content"> <?php for ($i = 0; $i < count($model['data']['bookmarks']['items']); $i++): ?> - <?php watchlistCard($model['data']['bookmarks']['items'][$i], $model["data"]["userUUID"], true, false, $i < 4 ? "eager" : "lazy"); ?> + <?php watchlistCard($model['data']['bookmarks']['items'][$i], $model["data"]["userUUID"], $i < 4 ? "eager" : "lazy"); ?> <?php endfor; ?> <?php pagination($model['data']['bookmarks']['page'], $model['data']['bookmarks']['totalPage']); ?> </section> diff --git a/src/server/app/View/profile/watchlist.php b/src/server/app/View/profile/watchlist.php index f4a4a066cfd56baeb9f8858db4eb2a0127146e1e..f8a6768104e58eabe0d4405890126a77720c3a2f 100644 --- a/src/server/app/View/profile/watchlist.php +++ b/src/server/app/View/profile/watchlist.php @@ -1,6 +1,6 @@ <?php -function watchlistCard(array $item, string $userUUID, bool $saved = false, bool $loved = false, string $loading = "eager") +function watchlistCard(array $item, string $userUUID, string $loading = "eager") { $uuid = $item["watchlist_uuid"]; $posters = $item["posters"]; @@ -12,6 +12,8 @@ function watchlistCard(array $item, string $userUUID, bool $saved = false, bool $description = $item["description"]; $itemCount = $item["item_count"]; $loveCount = $item["like_count"]; + $loved = $item["liked"]; + $saved = true; $self = ($userUUID == $item["creator_uuid"]); require __DIR__ . '/../components/card/watchlistCard.php'; @@ -30,11 +32,11 @@ function pagination(int $currentPage, int $totalPage) <h2>My Watchlist</h2> <div class="visibility"> <a id="visibility-all" href="/profile/watchlist?visibility=all" - class="btn <?= $model['data']['visibility'] === "all" ? "selected" : "" ?>">All</a> + class="btn <?= $model['data']['visibility'] === "all" ? "selected" : "" ?>">All</a> <a id="visibility-private" href="/profile/watchlist?visibility=private" - class="btn <?= $model['data']['visibility'] === "private" ? "selected" : "" ?>">Private</a> + class="btn <?= $model['data']['visibility'] === "private" ? "selected" : "" ?>">Private</a> <a id="visibility-public" href="/profile/watchlist?visibility=public" - class="btn <?= $model['data']['visibility'] === "public" ? "selected" : "" ?>">Public</a> + class="btn <?= $model['data']['visibility'] === "public" ? "selected" : "" ?>">Public</a> </div> </div> <a href="/watchlist/create" class="btn btn-bold"> @@ -55,7 +57,7 @@ function pagination(int $currentPage, int $totalPage) <?php endif; ?> <section class="content"> <?php for ($i = 0; $i < count($model['data']['watchlists']['items']); $i++): ?> - <?php watchlistCard($model['data']['watchlists']['items'][$i], $model["data"]["userUUID"], true, false, $i < 4 ? "eager" : "lazy",); ?> + <?php watchlistCard($model['data']['watchlists']['items'][$i], $model["data"]["userUUID"], $i < 4 ? "eager" : "lazy", ); ?> <?php endfor; ?> <?php pagination($model['data']['watchlists']['page'], $model['data']['watchlists']['totalPage']); ?> </section> diff --git a/src/server/app/View/watchlist/detail.php b/src/server/app/View/watchlist/detail.php index 60a8a54a233701a6eb857ea91d2c0931a84fcc15..385ec49549242e59fa8d61a9bffbc29ff70fcf03 100644 --- a/src/server/app/View/watchlist/detail.php +++ b/src/server/app/View/watchlist/detail.php @@ -41,6 +41,13 @@ if (!function_exists("likeAndSave")) { <article class="header"> <div class="detail"> <h2> + <?php if ($model['data']['item']['visibility'] === "PRIVATE"): ?> + <svg xmlns="http://www.w3.org/2000/svg" width="12" height="14" viewBox="0 0 12 14" fill="none"> + <path + d="M9 7H10.05C10.2985 7 10.5 7.20145 10.5 7.45V12.55C10.5 12.7985 10.2985 13 10.05 13H1.95C1.70147 13 1.5 12.7985 1.5 12.55V7.45C1.5 7.20145 1.70147 7 1.95 7H3M9 7V4C9 3 8.4 1 6 1C3.6 1 3 3 3 4V7M9 7H3" + stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> + </svg> + <?php endif; ?> <?= $model['data']['item']['title'] ?> </h2> <div class="container-subtitle"> @@ -54,7 +61,9 @@ if (!function_exists("likeAndSave")) { </div> <div class="watchlist__wrapper-type-author"> <?php foreach ($model["data"]["item"]["tags"] as $tag): ?> - <span class="tag"><?= $tag["name"] ?></span> + <span class="tag"> + <?= $tag["name"] ?> + </span> <?php endforeach; ?> </div> <p> @@ -67,7 +76,7 @@ if (!function_exists("likeAndSave")) { <?php likeAndSave("btn__like", "love"); ?> <?php else: ?> <button class="btn-ghost btn__like" data-id="<?= $model["data"]["item"]["watchlist_uuid"] ?>" - data-liked="<?= $model["data"]["item"]["liked"] ?>"> + data-liked="<?= $model["data"]["item"]["liked"] ?>"> <?php $type = (isset($model['data']["item"]['liked']) && $model['data']["item"]['liked']) ? "filled" : "unfilled"; require PUBLIC_PATH . 'assets/icons/love.php' ?> @@ -83,8 +92,7 @@ if (!function_exists("likeAndSave")) { <?php likeAndSave("btn__save", "bookmark"); ?> <?php else: ?> <button class="btn-ghost btn__save" data-id="<?= $model["data"]["item"]["watchlist_uuid"] ?>" - data-saved="<?= $model["data"]["item"]["saved"] ?>" - > + data-saved="<?= $model["data"]["item"]["saved"] ?>"> <?php $type = (isset($model['data']['item']['saved']) && $model['data']['item']['saved']) ? "filled" : "unfilled"; require PUBLIC_PATH . 'assets/icons/bookmark.php' ?> @@ -103,12 +111,11 @@ if (!function_exists("likeAndSave")) { <?php if (isset($model['data']['userUUID']) && $model['data']['userUUID'] === $model['data']['item']['creator_uuid']): ?> <div class="watchlist-detail__button-container"> <a href="<?= "/watchlist/" . $model['data']['item']['watchlist_uuid'] . "/edit" ?>" id="edit" - aria-label="Edit <?= $model['data']['item']['title'] ?>" class="btn-icon"> + aria-label="Edit <?= $model['data']['item']['title'] ?>" class="btn-icon"> <?php require PUBLIC_PATH . 'assets/icons/edit.php' ?> </a> <button type="submit" aria-label="Delete <?= $model['data']['item']['title'] ?>" - class="dialog-trigger btn-icon btn__delete" - data-id="<?= $model["data"]["item"]["watchlist_uuid"] ?>"> + class="dialog-trigger btn-icon btn__delete" data-id="<?= $model["data"]["item"]["watchlist_uuid"] ?>"> <?php require PUBLIC_PATH . 'assets/icons/trash.php' ?> </button> </div> diff --git a/src/server/routes/view.php b/src/server/routes/view.php index ea94b3e86be137c7459e0794b9a8f185ec698754..850d114035dbdc88c0174227ae6e11552fc51d30 100644 --- a/src/server/routes/view.php +++ b/src/server/routes/view.php @@ -26,19 +26,17 @@ Router::add('GET', '/signin', UserController::class, 'signIn', []); Router::add('POST', '/signin', UserController::class, 'postSignIn', []); Router::add('POST', '/api/auth/logout', UserController::class, 'logout', [UserAuthMiddleware::class]); Router::add('DELETE', '/api/auth/delete', UserController::class, 'delete', [UserAuthMiddleware::class]); -Router::add('POST', '/api/auth/update', UserController::class, 'update', [UserAuthMiddleware::class]); +Router::add('PUT', '/api/auth/update', UserController::class, 'update', [UserAuthMiddleware::class]); // Catalog controllers Router::add('GET', '/catalog', CatalogController::class, 'index', []); Router::add('GET', '/catalog/create', CatalogController::class, 'create', [AdminAuthMiddleware::class]); -Router::add('POST', '/catalog/create', CatalogController::class, 'postCreate', [AdminAuthMiddleware::class]); -Router::add('GET', '/catalog/([A-Za-z0-9]*)/edit', CatalogController::class, 'edit', [AdminAuthMiddleware::class]); -Router::add('POST', '/catalog/([A-Za-z0-9]*)/edit', CatalogController::class, 'postEdit', [AdminAuthMiddleware::class]); -Router::add('POST', '/catalog/([A-Za-z0-9]*)/delete', CatalogController::class, 'postDelete', [AdminAuthMiddleware::class]); -Router::add('GET', '/catalog/([A-Za-z0-9]*)', CatalogController::class, 'detail', []); +Router::add('GET', '/catalog/([A-Za-z0-9\-]*)', CatalogController::class, 'detail', []); +Router::add('GET', '/catalog/([A-Za-z0-9\-]*)/edit', CatalogController::class, 'edit', [AdminAuthMiddleware::class]); +Router::add('POST', '/api/catalog/create', CatalogController::class, 'postCreate', [AdminAuthMiddleware::class]); Router::add('GET', '/api/catalog', CatalogController::class, "search", [UserAuthApiMiddleware::class]); -Router::add("DELETE", "/api/catalog/([A-Za-z0-9]*)/delete", CatalogController::class, "delete", [AdminAuthMiddleware::class]); -Router::add("POST", '/api/catalog/([A-Za-z0-9]*)/update', CatalogController::class, 'update', [AdminAuthMiddleware::class]); +Router::add("DELETE", "/api/catalog/([A-Za-z0-9\-]*)/delete", CatalogController::class, "delete", [AdminAuthMiddleware::class]); +Router::add("POST", '/api/catalog/([A-Za-z0-9\-]*)/update', CatalogController::class, 'update', [AdminAuthMiddleware::class]); // Watchlist controllers Router::add("GET", "/watchlist/create", WatchlistController::class, 'create', [UserAuthMiddleware::class]);