diff --git a/src/migration/db.sql b/src/migration/db.sql index 432c9cb3e159b46022b028f7e8cf1fae1e2bcec5..14fc6159cb1cb3697b2f5810931d6c8acbf82a7c 100644 --- a/src/migration/db.sql +++ b/src/migration/db.sql @@ -7,7 +7,7 @@ END $$; CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY AUTO_INCREMENT=2, + id SERIAL PRIMARY KEY, uuid VARCHAR(36) NOT NULL UNIQUE, name VARCHAR(40) NOT NULL, password VARCHAR(255) NOT NULL, @@ -196,4 +196,36 @@ CREATE TABLE IF NOT EXISTS watchlist_tag ( FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE ); -INSERT INTO users (id, uuid, name, password, email, role) VALUES (1, '01c9b7cee28bc050', 'admin', 'admin', 'admin@drawl.com', 'ADMIN'); \ No newline at end of file +INSERT INTO users (uuid, name, password, email, role) VALUES ('72c5a265715fa26c', 'admin', '$2y$10$rAfDHA4M4ftn8K7Wx82wf.fFODD7PCE/t9CVnBwdLnTDBYjnq7ZnO', 'admin@drawl.com', 'ADMIN'); +INSERT INTO tags(name) VALUES ('ACTION'), + ('ADVENTURE'), + ('ANIMALS'), + ('BUSINESS'), + ('COMEDY'), + ('CRIME'), + ('DETECTIVE'), + ('DOCUMENTARY'), + ('DRAMA'), + ('FAMILY'), + ('FANTASY'), + ('FOOD'), + ('HISTORICAL'), + ('HORROR'), + ('LAW'), + ('LIFE'), + ('MANGA'), + ('MEDICAL'), + ('MATURE'), + ('MYSTERY'), + ('MUSIC'), + ('MILITARY'), + ('MELODRAMA'), + ('PSYCHOLOGICAL'), + ('ROMANCE'), + ('SCHOOL'), + ('SCI-FI'), + ('SPORTS'), + ('SUPERNATURAL'), + ('THRILLER'), + ('YOUTH') +; \ No newline at end of file diff --git a/src/public/assets/icons/user.php b/src/public/assets/icons/user.php new file mode 100644 index 0000000000000000000000000000000000000000..0084567817ae83f90d4986c308c750df18a81dbc --- /dev/null +++ b/src/public/assets/icons/user.php @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="21" height="22" viewBox="0 0 21 22" fill="none"> + <path + d="M10.5 1.5C5.25329 1.5 1 5.75329 1 11C1 16.2467 5.25329 20.5 10.5 20.5C15.7467 20.5 20 16.2467 20 11C20 5.75329 15.7467 1.5 10.5 1.5Z" + stroke="#1E2124" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> + <path d="M3.15747 17.0284C3.15747 17.0284 5.27504 14.325 10.5 14.325C15.725 14.325 17.8427 17.0284 17.8427 17.0284" + stroke="#1E2124" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> + <path + d="M10.4999 11C12.074 11 13.3499 9.7241 13.3499 8.15005C13.3499 6.57604 12.074 5.30005 10.4999 5.30005C8.92585 5.30005 7.6499 6.57604 7.6499 8.15005C7.6499 9.7241 8.92585 11 10.4999 11Z" + stroke="#1E2124" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> +</svg> \ No newline at end of file diff --git a/src/public/css/catalog-detail.css b/src/public/css/catalog-detail.css index b3357a7b1f53ac326250fcaf95dae9e5a2e31f92..9c1716ce00c6d8012fdd077ceb00c34f9ecebc1b 100644 --- a/src/public/css/catalog-detail.css +++ b/src/public/css/catalog-detail.css @@ -25,12 +25,6 @@ overflow-wrap: break-word; } -.catalog-trailer { - width: 80%; - max-width: 600px; - align-self: stretch; -} - .poster { margin-top: -100px; margin-left: 12px; @@ -77,7 +71,7 @@ .button-container { bottom: 40px; - right: 160px; + right: 3rem; gap: 10px; } } diff --git a/src/public/css/components/alert.css b/src/public/css/components/alert.css deleted file mode 100644 index b754f83ebe39def252b892bb9b0be7183f5bc0f3..0000000000000000000000000000000000000000 --- a/src/public/css/components/alert.css +++ /dev/null @@ -1,96 +0,0 @@ -.alert { - display: flex; - padding: 16px 6px; - justify-content: space-between; - align-items: flex-start; - align-self: stretch; - border-radius: 6px; -} - -.alert[data-type="error"] { - background-color: var(--error-background); -} - -.alert[data-type="error"] h3 { - color: var(--error-foreground); -} - -.alert[data-type="error"] p { - color: var(--error-foreground); -} - -.alert[data-type="error"] svg path { - stroke: var(--error-foreground); -} - -.alert[data-type="info"] { - background-color: var(--info-background); -} - -.alert[data-type="info"] h3 { - color: var(--info-foreground); -} - -.alert[data-type="error"] p { - color: var(--error-foreground); -} - -.alert[data-type="info"] svg path { - stroke: var(--info-foreground); -} - -.alert[data-type="success"] { - background-color: var(--success-background); -} - -.alert[data-type="success"] h3 { - color: var(--success-foreground); -} - -.alert[data-type="success"] svg path { - stroke: var(--success-foreground); -} - -.alert div { - display: flex; - padding: 0px 12px; - flex-direction: column; - align-items: flex-start; - gap: 6px; - flex: 1 0 0; -} - -.alert h3 { - font-size: 0.875rem; -} - -.alert p { - font-size: 0.625rem; -} - -.alert>svg { - display: none; -} - -@media screen and (min-width: 640px) { - .alert { - padding: 24px 20px; - } - - .alert h3 { - font-size: 1rem; - } - - .alert p { - font-size: 0.875rem; - } - - .alert>svg { - display: block; - } - - .alert div { - padding: 0px 24px; - gap: 10px; - } -} \ No newline at end of file diff --git a/src/public/css/components/button.css b/src/public/css/components/button.css deleted file mode 100644 index b00a447113164684dc88eb06f7efce00c554e2e4..0000000000000000000000000000000000000000 --- a/src/public/css/components/button.css +++ /dev/null @@ -1,101 +0,0 @@ -button { - background: none; - border: none; - cursor: pointer; - border-radius: 6px; - padding: 8px 16px; - gap: 8px; - display: flex; - justify-content: center; - align-items: center; - font-weight: 600; - white-space: nowrap; - text-transform: uppercase; -} - -button:hover { - background-color: var(--accent-300); - transition: all; - transition-duration: 400ms; - transition-timing-function: ease-in-out; -} - -.btn { - background: none; - border: none; - font: inherit; - color: inherit; - cursor: pointer; - border-radius: 6px; - padding: 8px 16px; - gap: 8px; - display: flex; - justify-content: center; - align-items: center; - font-weight: 600; - white-space: nowrap; - text-transform: uppercase; -} - -.btn:hover { - background-color: var(--accent-300); - transition: all; - transition-duration: 400ms; - transition-timing-function: ease-in-out; -} - -.btn-icon { - padding: 10px 10px; -} - -.btn-bold { - background: var(--accent-1100, #421B50); - color: var(--accent-100, #FBF7FD); -} - -.btn-bold:hover { - background-color: var(--accent-1000, #5F346F); - transition: all; - transition-duration: 400ms; - transition-timing-function: ease-in-out; -} - -.btn-primary { - display: inline-flex; - width: 100%; - max-width: fit-content; - text-decoration: none; - color: var(--primary-400); - background-color: var(--accent-300); - padding: 8px 12px; - border-radius: 5px; - border: none; - cursor: pointer; - align-items: center; - justify-content: center; - gap: 0.4rem; - font-size: 1rem; -} - -.btn-primary:hover { - background-color: var(--accent-300); - transition: all; - transition-duration: 400ms; - transition-timing-function: ease-in-out; -} - -.btn-text { - color: var(--accent-800); -} - -.btn-ghost:hover { - background-color: transparent; -} - -.btn-outline { - border: 1px solid rgba(0,0,0,.1); -} - -.btn-outline:hover { - background-color: var(--accent-100); -} \ No newline at end of file diff --git a/src/public/css/components/card.css b/src/public/css/components/card.css deleted file mode 100644 index abe72d8d54bf4ca0c4d0b4340f6449168a77e178..0000000000000000000000000000000000000000 --- a/src/public/css/components/card.css +++ /dev/null @@ -1,52 +0,0 @@ -.card { - width: 100%; - display: flex; - flex-direction: column; - align-items: flex-end; - align-self: stretch; - justify-content: space-between; - padding-bottom: 24px; - gap: 24px; - border-bottom: 1px solid var(--slate-100, #CBD5E1); -} - -.card-content { - width: 100%; - display: flex; - align-items: flex-start; - gap: 24px; -} - -.card-body { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 10px; - flex: 1 0 0; -} - -.card-body > p { - overflow: hidden; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 3; -} - -.card-button-container { - display: flex; - align-items: flex-start; - gap: 24px; -} - -.card-comment .card-content .card-body .header { - display: flex; - align-items: center; - gap: 10px; -} - -@media screen and (min-width: 640px) { - .card { - flex-direction: row; - align-items: flex-start; - } -} \ No newline at end of file diff --git a/src/public/css/components/dialog.css b/src/public/css/components/dialog.css deleted file mode 100644 index 98d7c6c7bc10f8687c12309abf7693d5d319b7fc..0000000000000000000000000000000000000000 --- a/src/public/css/components/dialog.css +++ /dev/null @@ -1,40 +0,0 @@ -.dialog { - position: fixed; - z-index: 100; - top: 0; - left: 0; - display: none; - width: 100vw; - height: 100vh; - justify-content: center; - align-items: center; - background: rgba(15, 23, 42, 0.20); -} - -.dialog-content { - display: flex; - max-width: 900px; - padding: 32px 40px; - flex-direction: column; - align-items: flex-start; - gap: 20px; - flex-shrink: 0; - border-radius: 10px; - background: #FFF; -} - -.dialog-button-container { - width: 100%; - display: flex; - justify-content: flex-end; - gap: 10px; - margin-top: 20px; -} - -.is-active { - display: flex; -} - -body:has(.dialog.is-active) { - overflow: hidden; -} \ No newline at end of file diff --git a/src/public/css/components/form.css b/src/public/css/components/form.css deleted file mode 100644 index a09bf3c4c947f52fd041fb95b4a90ef298eb0f1f..0000000000000000000000000000000000000000 --- a/src/public/css/components/form.css +++ /dev/null @@ -1,30 +0,0 @@ -.form-default { - display: flex; - flex-direction: column; - width: 100%; - gap: 1rem; -} - -.form-input-default { - display: flex; - flex-direction: column; - gap: 0.4rem; -} - -.form-input-radio-default { - display: flex; - align-items: center; - gap: 0.8rem; -} - -.input-default { - padding: 0.75rem; - border-radius: 0.4rem; - border: 1px solid rgba(0,0,0,.1); - width: 100%; -} - -.input-default:focus { - outline: none; - outline: 1px solid var(--accent-800); -} \ No newline at end of file diff --git a/src/public/css/components/icon.css b/src/public/css/components/icon.css deleted file mode 100644 index 6842951beeef1e0b636d05ed4b4f0eb1b72f1eb2..0000000000000000000000000000000000000000 --- a/src/public/css/components/icon.css +++ /dev/null @@ -1,21 +0,0 @@ -.icon-heart:hover { - fill: var(--accent-600); - transition: all; - transition-duration: 400ms; - transition-timing-function: ease-in-out; -} - -.icon-heart[data-type="filled"] { - fill: var(--accent-600); -} - -.icon-bookmark:hover { - fill: var(--accent-600); - transition: all; - transition-duration: 400ms; - transition-timing-function: ease-in-out; -} - -.icon-bookmark[data-type="filled"] { - fill: var(--accent-600); -} \ No newline at end of file diff --git a/src/public/css/components/input.css b/src/public/css/components/input.css deleted file mode 100644 index cf697682b68fc3d5a7d111d375f67d0d1c266c3b..0000000000000000000000000000000000000000 --- a/src/public/css/components/input.css +++ /dev/null @@ -1,46 +0,0 @@ -.input-group { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 10px; -} - -.input-required::after { - content: '*'; - color: #FC7979; - margin-left: 8px; -} - -input { - width: 100%; - display: flex; - height: 36px; - padding: 8px 56px 8px 12px; - align-items: flex-start; - align-self: stretch; - border-radius: 6px; - border: 1px solid var(--slate-200, #CBD5E1); - background: #FFF; - font-size: 1rem; - font-style: normal; -} - -label { - color: var(--slate-900, #0F172A); - font-size: 0.875rem; - font-style: normal; - font-weight: 400; -} - -textarea { - display: flex; - min-height: 100px; - padding: 8px 56px 8px 12px; - align-items: flex-start; - flex: 1 0 0; - align-self: stretch; - border-radius: 6px; - border: 1px solid var(--slate-200, #CBD5E1); - background: #FFF; - font-size: 1rem; -} \ No newline at end of file diff --git a/src/public/css/components/modal.css b/src/public/css/components/modal.css deleted file mode 100644 index 488bbf5c0344f16bc0c0e929c79739a6ebf0b7ea..0000000000000000000000000000000000000000 --- a/src/public/css/components/modal.css +++ /dev/null @@ -1,54 +0,0 @@ -.modal { - width: 100%; - display: flex; - align-items: center; - flex-direction: column; -} - -.modal__trigger { - width: 100%; -} - - -.modal__content { - display: flex; - gap: 1rem; - position: relative; - width: 100%; - max-width: 40rem; - margin: 1rem; - flex-direction: column; - padding: 1rem; - background-color: white; - border-radius: 0.4rem; - max-height: 90vh; - overflow: auto; - box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; -} - -.modal__backdrop { - position: fixed; - display: none; - align-items: center; - justify-content: center; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 1000; - width: 100%; - height: 100vh; - background-color: rgba(255, 255, 255, .5); - backdrop-filter: blur(4px); -} - -.modal__close { - position: absolute; - top: 10px; - right: 10px; - max-width: fit-content; - padding: 0; -} - -.icon-x { - color: rgba(0,0,0,.4); -} \ No newline at end of file diff --git a/src/public/css/components/pagination.css b/src/public/css/components/pagination.css deleted file mode 100644 index 9793a3d58f4c8ac94fab31dbaaefd73fc933249f..0000000000000000000000000000000000000000 --- a/src/public/css/components/pagination.css +++ /dev/null @@ -1,46 +0,0 @@ -.pagination { - width: 100%; - display: flex; - align-items: center; - justify-content: center; - margin-top: 80px; -} - -.pagination-elips { - display: flex; - justify-content: center; - align-items: center; - gap: 10px; - border-radius: 8px; - width: 36px; - height: 36px; -} - -.pagination-item { - display: flex; - justify-content: center; - align-items: center; - gap: 10px; - border-radius: 8px; - width: 36px; - height: 36px; -} - -.pagination-item:hover { - background-color: var(--accent-300); - transition: all; - transition-duration: 400ms; - transition-timing-function: ease-in-out; -} - -.pagination-item[data-type="active"] { - background: var(--accent-300, #EDDEF6); -} - -.pagination-item.prev { - transform: rotate(90deg); -} - -.pagination-item.next { - transform: rotate(-90deg); -} \ No newline at end of file diff --git a/src/public/css/components/select.css b/src/public/css/components/select.css deleted file mode 100644 index f31d8847eac3024e80a2fcd9652ee2c929b48c5e..0000000000000000000000000000000000000000 --- a/src/public/css/components/select.css +++ /dev/null @@ -1,44 +0,0 @@ -.c-select-menu { - min-width: 200px; - width: 100%; - position: relative; -} - -.c-select-menu .c-select-btn { - display: flex; - padding: 0.35rem; - border: 1px solid rgba(0,0,0,.1); - border-radius: 0.4rem; - align-items: center; - justify-content: space-between; - cursor: pointer; - position: relative; -} - -.c-select-menu .c-select-options { - position: absolute; - padding: 0.4rem; - top: 3rem; - background-color: #fff; - border: 1px solid rgba(0,0,0,0.1); - border-radius: 0.4rem; - min-width: 200px; - width: 100%; - z-index: 50; -} - -.c-select-options .c-select-option { - display: flex; - padding: 0.6rem; - cursor: pointer; - align-items: center; - border-radius: 0.4rem; -} - -.c-select-options .c-select-option:hover { - background-color: var(--accent-100); -} - -.c-select-hide { - display: none; -} \ No newline at end of file diff --git a/src/public/css/components/textarea.css b/src/public/css/components/textarea.css deleted file mode 100644 index 3381a355f56400673c67128142287afef647430b..0000000000000000000000000000000000000000 --- a/src/public/css/components/textarea.css +++ /dev/null @@ -1,3 +0,0 @@ -textarea { - font-size: 1rem; -} \ No newline at end of file diff --git a/src/public/css/global.css b/src/public/css/global.css index d265d12fba64cf32d30820bd02be9e9876008853..3587c8b678006575469d9a200fe00c50f48ca85d 100644 --- a/src/public/css/global.css +++ b/src/public/css/global.css @@ -367,6 +367,31 @@ button:hover { display: none; } +.navbar-menu .profile-menu { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 1rem; + background-color: #FFF; +} + +.navbar-menu .profile-menu a, +.navbar-menu .profile-menu button { + width: 100%; + align-items: flex-start; + justify-content: flex-start; +} + +.navbar-menu .profile-menu.collapsed { + display: flex; +} + +.navbar-menu:not(.collapsed) .profile-icon { + display: none; +} + .navbar-menu a { width: 100%; align-items: flex-start; @@ -387,16 +412,29 @@ button:hover { background-color: transparent; position: static; flex-direction: row; - align-items: flex-end; + align-items: center; justify-content: flex-end; box-shadow: none; gap: 0; } + .navbar-menu .profile-menu { + width: fit-content; + position: absolute; + top: 80px; + padding: 1rem 1.5rem; + border-radius: 6px; + box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px; + } + .navbar-menu.collapsed { display: flex; } + .navbar-menu .profile-menu.collapsed { + display: none; + } + .navbar-menu a { width: auto; } @@ -682,6 +720,8 @@ button:hover { min-width: 200px; width: 100%; z-index: 50; + max-height: 20rem; + overflow: auto; } .c-select-options .c-select-option { @@ -1018,6 +1058,7 @@ textarea:focus { display: flex; flex-direction: row; gap: 0.6rem; + flex-wrap: wrap; } .watchlist__meta { @@ -1150,4 +1191,100 @@ textarea:focus { .no-item__container span { filter: blur(2px); +} + +/* Toast */ + +.toast { + min-width: 250px; + max-width: 80vw; + background-color: var(--error-background); + color: var(--error-foreground); + text-align: center; + border-radius: 6px; + display: flex; + padding: 16px 6px; + justify-content: space-between; + align-items: flex-start; + align-self: stretch; + position: fixed; + z-index: 100; + margin: 0 auto; + left: 0; + right: 0; + top: 8px; +} + +.toast[data-type="error"] { + background-color: var(--error-background); +} + +.toast[data-type="error"] h3 { + color: var(--error-foreground); +} + +.toast[data-type="error"] p { + color: var(--error-foreground); +} + +.toast[data-type="error"] svg path { + stroke: var(--error-foreground); +} + +.toast[data-type="info"] { + background-color: var(--info-background); +} + +.toast[data-type="info"] h3 { + color: var(--info-foreground); +} + +.toast[data-type="error"] p { + color: var(--error-foreground); +} + +.toast[data-type="info"] svg path { + stroke: var(--info-foreground); +} + +.toast[data-type="success"] { + background-color: var(--success-background); +} + +.toast[data-type="success"] h3 { + color: var(--success-foreground); +} + +.toast[data-type="success"] svg path { + stroke: var(--success-foreground); +} + +.toast div { + display: flex; + padding: 0px 12px; + flex-direction: column; + align-items: flex-start; + gap: 6px; + flex: 1 0 0; +} + + +.toast p { + text-align: start; +} + +.toast > svg { + display: none; +} + +@media screen and (min-width: 640px) { + .toast { + max-width: 400px; + } +} + +.catalog-trailer { + width: 80%; + max-width: 600px; + align-self: stretch; } \ No newline at end of file diff --git a/src/public/css/signIn.css b/src/public/css/signIn.css index 125d794051b95d1d61730526fb611775053dde1f..d71a618dc5842cd69e27dfc4e3939be0dd851b10 100644 --- a/src/public/css/signIn.css +++ b/src/public/css/signIn.css @@ -9,6 +9,7 @@ } .signin-poster { + display: none; width: 50%; height: 100vh; object-fit: cover; @@ -25,13 +26,15 @@ } .main-container { + max-width: 500px; + width: 90%; padding: 24px; background: white; border-radius: 6px; flex-direction: column; justify-content: center; align-items: center; - gap: 80px; + gap: 40px; display: flex; } @@ -80,17 +83,8 @@ margin-bottom: 20px; } - - -input { - align-self: stretch; - padding: 8px 8px 12px 56px; - background: white; - border-radius: 6px; - border: 1px #cbd5e1 solid; - justify-content: flex-start; - align-items: center; - display: inline-flex; +form p:has(a) { + text-align: center; } @@ -102,7 +96,7 @@ input { } @media screen and (min-width: 1024px) { - .signup-poster { + .signin-poster { display: block; } } diff --git a/src/public/css/signUp.css b/src/public/css/signUp.css index 2ab624121da843ec783ff0432ca885ebddfb7d8b..c13558166cf80593adfb3a075e2a3534b2b682a6 100644 --- a/src/public/css/signUp.css +++ b/src/public/css/signUp.css @@ -9,6 +9,7 @@ } .signup-poster { + display: none; width: 50%; height: 100vh; object-fit: cover; @@ -25,13 +26,15 @@ } .main-container { + max-width: 500px; + width: 90%; padding: 24px; background: white; border-radius: 6px; flex-direction: column; justify-content: center; align-items: center; - gap: 80px; + gap: 40px; display: flex; } @@ -78,19 +81,11 @@ -input { - align-self: stretch; - padding: 8px 8px 12px 56px; - background: white; - border-radius: 6px; - border: 1px #cbd5e1 solid; - justify-content: flex-start; - align-items: center; - display: inline-flex; +form p:has(a) { + text-align: center; } - @media screen and (min-width: 640px) { .welcome-text__h2 { font-size: 2rem; @@ -102,3 +97,9 @@ input { display: block; } } + +.signin-link { + color: var(--accent-800); + font-size: 14px; + text-decoration: underline; +} \ No newline at end of file diff --git a/src/public/css/watchlist-detail.css b/src/public/css/watchlist-detail.css index 64226f73f911b8d4ada95106e0197513d21a0070..99a0a6d9c05c8d8c0d069540fb03f87a8672922f 100644 --- a/src/public/css/watchlist-detail.css +++ b/src/public/css/watchlist-detail.css @@ -22,8 +22,10 @@ article.header .container-subtitle { } article.header .container-button { + width: 100%; display: flex; align-items: flex-start; + justify-content: center; } .container-btn-love { @@ -84,7 +86,11 @@ form { gap: 10px; } + article.header .container-button { + justify-content: flex-end; + } + article.header .detail { width: 80%; - } + } } diff --git a/src/public/css/watchlistCreate.css b/src/public/css/watchlistCreate.css index 0dc427fe85b705166af111a418e32d85e3ed2a5b..03c12711d09cc3847fd4d1ebd98e09d151afaa5c 100644 --- a/src/public/css/watchlistCreate.css +++ b/src/public/css/watchlistCreate.css @@ -20,6 +20,25 @@ gap: 0.6rem; } +.tags { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.2rem; + margin: 0.2rem 0; +} + +.checkbox { + max-width: fit-content; +} + +.input-tag { + display: flex; + flex-direction: row-reverse; + align-items: center; + justify-content: start; + gap: 1rem; +} + .watchlist-items__title { margin: 1rem 0 0 0; } @@ -62,11 +81,18 @@ flex-grow: 1; } +@media screen and (min-width: 784px) { + .tags { + grid-template-columns: repeat(5, minmax(0, 1fr)); + } +} + @media screen and (min-width: 1024px) { .container__create-watchlist { flex-direction: row; } + .actions { position: sticky; top: 8rem; diff --git a/src/public/js/components/dialog.js b/src/public/js/components/dialog.js deleted file mode 100644 index 534c1a3245ae959df55a1e105ab174d071c6cd8e..0000000000000000000000000000000000000000 --- a/src/public/js/components/dialog.js +++ /dev/null @@ -1,26 +0,0 @@ -dialogTriggers = document.getElementsByClassName("dialog-trigger"); -dialogs = document.getElementsByClassName("dialog"); - -for (let i = 0; i < dialogTriggers.length; i++) { - dialogTriggers[i].addEventListener("click", () => { - dialogs[i].classList.remove("hidden"); - }); -} - -for (let i = 0; i < dialogs.length; i++) { - dialogs[i].querySelector("#cancel").addEventListener("click", () => { - dialogs[i].classList.add("hidden"); - }); -} -// if (dialogTrigger) { -// dialogTrigger.addEventListener("click", () => { -// document.querySelector(".dialog").classList.add("is-active"); -// }); -// } - -// dialogClose = document.querySelector(".dialog #cancel"); -// if (dialogClose) { -// dialogClose.addEventListener("click", () => { -// document.querySelector(".dialog").classList.remove("is-active"); -// }); -// } diff --git a/src/public/js/components/modal/watchlistAddItem.js b/src/public/js/components/modal/watchlistAddItem.js index 268473bfadd4c6bae5586155d7238c139e7148e2..4668a35799599bf35be756f12234aea0ec7d7d09 100644 --- a/src/public/js/components/modal/watchlistAddItem.js +++ b/src/public/js/components/modal/watchlistAddItem.js @@ -22,6 +22,13 @@ function deleteItemAction(id) { btnAddToList.innerHTML = PLUS_ICON; } item.remove(); + + if (catalogSelected.length === 0) { + const itemsPlaceholder = document.createElement("p"); + itemsPlaceholder.classList.add("items-placeholder"); + itemsPlaceholder.textContent = "No items selected."; + watchlistItemContainer.appendChild(itemsPlaceholder); + } } } @@ -113,6 +120,8 @@ function fetchSearch(replace = false) { const xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function () { if (xhttp.readyState === 4) { + const itemsPlaceholder = document.querySelector("p.items-placeholder"); + itemsPlaceholder.remove(); const wrapper = document.createElement('div'); wrapper.classList.add('watchlist-item'); wrapper.draggable = "true"; @@ -193,8 +202,4 @@ btnAddItem.addEventListener('click', () => { drag(); getCatalogSelected(); -deleteItem(); - - -console.log(catalogSelected); - +deleteItem(); \ No newline at end of file diff --git a/src/public/js/components/navbar.js b/src/public/js/components/navbar.js index b95cbebe24315a199f462ffec53463137ba41c35..c46ff5597fde538d9f9952ca44a4e3092bca7684 100644 --- a/src/public/js/components/navbar.js +++ b/src/public/js/components/navbar.js @@ -14,7 +14,11 @@ if (navbarToggle) { }); navbarToggle.addEventListener("blur", function (e) { - if (e.relatedTarget && e.relatedTarget.parentElement.id === "navbar-menu") { + if ( + e.relatedTarget && + (e.relatedTarget.parentElement.id === "navbar-menu" || + e.relatedTarget.parentElement.id === "profile-menu") + ) { return; } else { navbarMenu = document.getElementById("navbar-menu"); @@ -24,3 +28,33 @@ if (navbarToggle) { } }); } + +profileMenuToggle = document.getElementById("profile-menu-toggle"); +if (profileMenuToggle) { + profileMenuToggle.addEventListener("click", function () { + profileMenu = document.getElementById("profile-menu"); + if (profileMenu.classList.contains("collapsed")) { + profileMenu.classList.remove("collapsed"); + profileMenuToggle.focus(); + this.setAttribute("aria-expanded", "true"); + } else { + profileMenu.classList.add("collapsed"); + profileMenuToggle.blur(); + this.setAttribute("aria-expanded", "false"); + } + }); + + profileMenuToggle.addEventListener("blur", function (e) { + if ( + e.relatedTarget && + e.relatedTarget.parentElement.id === "profile-menu" + ) { + return; + } else { + profileMenu = document.getElementById("profile-menu"); + profileMenu.classList.add("collapsed"); + profileMenuToggle.blur(); + this.setAttribute("aria-expanded", "false"); + } + }); +} diff --git a/src/public/js/deleteCatalog.js b/src/public/js/deleteCatalog.js new file mode 100644 index 0000000000000000000000000000000000000000..6671a08ee4aea6f48cd77ed74e65a1308538f9e2 --- /dev/null +++ b/src/public/js/deleteCatalog.js @@ -0,0 +1,47 @@ +function deleteCatalog(uuid, title) { + const xhttp = new XMLHttpRequest(); + xhttp.open("DELETE", `/api/catalog/${uuid}/delete`, true); + xhttp.setRequestHeader("Content-Type", "application/json"); + + xhttp.onreadystatechange = function () { + if (xhttp.readyState === 4) { + if (xhttp.status === 200) { + showToast("Success", `Catalog ${title} deleted`, "success"); + setTimeout(() => { + window.location.reload(); + }, 1000); + } else { + try { + const response = JSON.parse(xhttp.responseText); + showToast("Error", response.message, "error"); + } catch (error) { + showToast("Error", "Something went wrong", "error"); + } + } + } + }; + + xhttp.send(); +} + +const deleteTriggerButtons = document.querySelectorAll( + `.catalog-delete-trigger` +); +deleteTriggerButtons.forEach((deleteTriggerButton) => { + if (deleteTriggerButton) { + deleteTriggerButton.addEventListener("click", () => { + const uuid = deleteTriggerButton.getAttribute("data-uuid"); + const title = deleteTriggerButton.getAttribute("data-title"); + dialog( + "Delete Catalog", + `Are you sure you want to delete ${title}?`, + uuid, + "delete", + "Delete", + () => { + deleteCatalog(uuid, title); + } + ); + }); + } +}); diff --git a/src/public/js/editCatalog.js b/src/public/js/editCatalog.js new file mode 100644 index 0000000000000000000000000000000000000000..8926631df87497c5cdf576b2c4e31f9e377123e5 --- /dev/null +++ b/src/public/js/editCatalog.js @@ -0,0 +1,47 @@ +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 1ab8b7f097e3262e7914bc6f3e6f6ba6b35844de..dfbb814ff0122f0d1f1efd62a876108a3ca7185f 100644 --- a/src/public/js/global.js +++ b/src/public/js/global.js @@ -1,3 +1,63 @@ +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; + } +} + +function dialog( + title, + message, + dialogId, + actionId, + actionButtonText, + onaction +) { + const body = document.querySelector("body"); + + const dialog = document.createElement("div"); + dialog.classList.add("dialog"); + dialog.id = `dialog-${dialogId}`; + dialog.innerHTML = ` + <div class="dialog__content"> + <h2> + ${title} + </h2> + <p> + ${message} + </p> + <div class="dialog__button-container"> + <button id="cancel"> + Cancel + </button> + <button id=${actionId} class="btn-bold"> + ${actionButtonText} + </button> + </div> + </div> + `; + + body.appendChild(dialog); + + const cancelButton = dialog.querySelector("#cancel"); + cancelButton.addEventListener("click", () => { + dialog.remove(); + }); + + 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 => { @@ -69,4 +129,42 @@ function save() { }) } }) -} \ No newline at end of file +} + +toastClose = document.querySelector("#toast button"); + +if (toastClose) { + toastClose.addEventListener("click", () => { + const toast = document.querySelector("#toast"); + toast.classList.add("hidden"); + }); +} + +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/home.js b/src/public/js/home.js index 22471e5b37c0df0ffeebbca2a33d15dc955b9997..a0a4c89ee4aa6fa833e21cf6d746b2e3a08b15d2 100644 --- a/src/public/js/home.js +++ b/src/public/js/home.js @@ -72,5 +72,4 @@ search = debounce(search, 500); inputSearch.addEventListener("keyup", search); like(); -save(); - +save(); \ No newline at end of file diff --git a/src/public/js/profile.js b/src/public/js/profile.js new file mode 100644 index 0000000000000000000000000000000000000000..23e1e12ed3cc5541ca97d6f1a5d8f59fe256b11d --- /dev/null +++ b/src/public/js/profile.js @@ -0,0 +1,89 @@ +function deleteAccount() { + const xhttp = new XMLHttpRequest(); + xhttp.open("DELETE", `/api/auth/delete`, true); + xhttp.setRequestHeader("Content-Type", "application/json"); + + xhttp.onreadystatechange = function () { + if (xhttp.readyState === 4) { + if (xhttp.status === 200) { + showToast("Success", `User deleted`, "success"); + setTimeout(() => { + window.location.reload(); + }, 1000); + } else { + try { + const response = JSON.parse(xhttp.responseText); + showToast("Error", response.message, "error"); + } catch (error) { + showToast("Error", "Something went wrong", "error"); + } + } + } + }; + + xhttp.send(); +} + +const deleteTriggerButton = document.getElementById("delete-account"); +if (deleteTriggerButton) { + deleteTriggerButton.addEventListener("click", () => { + dialog( + "Delete Account", + `Are you sure you want to delete your account?`, + "delete-account", + "delete", + "Delete", + () => { + deleteAccount(); + } + ); + }); +} + +function updateAccount(form) { + const xhttp = new XMLHttpRequest(); + xhttp.open("POST", `/api/auth/update`, true); + xhttp.setRequestHeader("Content-Type", "application/json"); + + xhttp.onreadystatechange = function () { + if (xhttp.readyState === 4) { + const response = JSON.parse(xhttp.responseText); + if (xhttp.status === 200 && response.status === 200) { + showToast("Success", `Profile updated`, "success"); + nameText = document.getElementById("name"); + nameText.innerText = response.name; + } else { + try { + const response = JSON.parse(xhttp.responseText); + showToast("Error", response.message, "error"); + } catch (error) { + showToast("Error", "Something went wrong", "error"); + } + } + } + }; + + xhttp.send( + JSON.stringify({ + name: form.name.value, + oldPassword: form.oldPassword.value, + newPassword: form.newPassword.value, + }) + ); +} + +const form = document.getElementById("profile-edit-form"); +form.addEventListener("submit", function (event) { + event.preventDefault(); + + dialog( + "Update Account", + `Are you sure you want to update your account?`, + "update", + "update", + "Confirm", + () => { + updateAccount(form); + } + ); +}); diff --git a/src/public/js/watchlist/createUpdate.js b/src/public/js/watchlist/createUpdate.js index 1ed7e05f49560a8058e804e35f8e72d25f33c4cc..1f848c0e167a00d31c6ba296f95115f843c74bf8 100644 --- a/src/public/js/watchlist/createUpdate.js +++ b/src/public/js/watchlist/createUpdate.js @@ -1,8 +1,65 @@ const formUpdateWatchlist = document.querySelector("form#update-watchlist"); +const VISIBILITY = ["PUBLIC", "PRIVATE"]; -formUpdateWatchlist.addEventListener("submit", e => { - e.preventDefault(); +function validateWatchlistCreateUpdateRequest(request) { + if (!request.title || request.title.trim() === "") { + return { + valid: false, + message: "Title is required." + }; + } + if (request.title.length > 40) { + return { + valid: false, + message: "Title is too long. Maximum 40 chars." + } + } + if (request.description && request.description.length > 255) { + return { + valid: false, + message: "Description is too long. Maximum 255 chars." + } + } + if (!request.visibility || !VISIBILITY.includes(request.visibility.trim())) { + return { + valid: false, + message: "Visibility is invalid." + } + } + if (!request.items || request.items.length === 0) { + return { + valid: false, + message: "Watchlist must contain 1 item." + } + } + if (request.items.length > 50) { + return { + valid: false, + message: "Too many items. Maximum 50 items." + } + } + + let maxDescExceeded = false; + let title; + for (let i = 0; i < request.items.length; i++) { + if (request.items[i].description.length > 255) { + title = request.items[i].title; + maxDescExceeded = true; + break; + } + } + if (maxDescExceeded) { + return { + valid: false, + message: `Description is too long for item ${title}. Maximum 255 chars.` + } + } + return { + valid: true + } +} +function createEditWatchlist() { // Parse url const currentUrl = window.location.href; const url = new URL(currentUrl); @@ -13,11 +70,13 @@ formUpdateWatchlist.addEventListener("submit", e => { const descriptionEl = document.querySelector("textarea#description"); const visibilityEl = document.querySelector("input#visibility"); const itemsEl = document.querySelectorAll("div.watchlist-item"); + const tagsEl = document.querySelectorAll("input.watchlist-tag"); const title = titleEl.value; const description = descriptionEl.value; const visibility = visibilityEl.value; const items = []; + const tags = []; itemsEl.forEach(item => { const itemDescEl = item.querySelector("textarea.watchlist-item__description"); @@ -38,15 +97,30 @@ formUpdateWatchlist.addEventListener("submit", e => { }) }) - // ajax request + tagsEl.forEach(item => { + if (item.checked) { + tags.push({ + id: item.value + }) + } + }) + // validate request let data = { title, description, visibility, - items + items, + tags + } + + let validationResult = validateWatchlistCreateUpdateRequest(data); + if (!validationResult.valid) { + showToast("Invalid Request", validationResult.message); + return; } + // ajax request if (action === "edit") { data["watchlistUUID"] = url.pathname.split("/")[2]; } @@ -60,17 +134,38 @@ formUpdateWatchlist.addEventListener("submit", e => { xhttp.onreadystatechange = function () { if (xhttp.readyState === 4) { + const response = JSON.parse(xhttp.response); if (xhttp.status !== 200) { - document.body.innerHTML = xhttp.response; + showToast("Invalid Request", response.message) } else { - const response = JSON.parse(xhttp.response); - if (response.redirectTo !== null && response.redirectTo !== undefined) { - window.history.pushState({}, "", "/signin"); - window.location.reload(); - } + showToast("Success", response.message, "success"); + setTimeout(() => { + if (response.redirectTo !== null && response.redirectTo !== undefined) { + window.location.href = response.redirectTo; + window.history.pushState({}, "", response.redirectTo); + window.location.reload(); + } + }, 1000) } } } xhttp.send(data); +} + +formUpdateWatchlist.addEventListener("submit", e => { + e.preventDefault(); + + dialog( + "Update Watchlist", + `Are you sure you want to update this watchlist?`, + "update", + "update", + "Confirm", + () => { + createEditWatchlist(); + } + ); + + }) \ No newline at end of file diff --git a/src/public/js/watchlist/delete.js b/src/public/js/watchlist/delete.js new file mode 100644 index 0000000000000000000000000000000000000000..88cf402962925ef6181e5a2b611ff70ee9aae884 --- /dev/null +++ b/src/public/js/watchlist/delete.js @@ -0,0 +1,33 @@ +const btnDelete = document.querySelector("button.btn__delete"); + +btnDelete.addEventListener("click", () => { + let data = { + watchlistUUID: btnDelete.dataset.id, + } + data = JSON.stringify(data); + + const xhttp = new XMLHttpRequest(); + + xhttp.open("DELETE", "/api/watchlist", true); + xhttp.setRequestHeader("Content-Type", "application/json"); + + xhttp.onreadystatechange = function () { + if (xhttp.readyState === 4) { + const response = JSON.parse(xhttp.response); + if (xhttp.status !== 200) { + showToast("Failed to Delete Watchlist", response.message); + } else { + showToast("Success", response.message, "success"); + setTimeout(() => { + if (response.redirectTo !== null && response.redirectTo !== undefined) { + window.location.href = response.redirectTo; + window.history.pushState({}, "", response.redirectTo); + window.location.reload(); + } + }, 1000) + } + } + } + + xhttp.send(data); +}) \ No newline at end of file diff --git a/src/public/js/watchlist/detail.js b/src/public/js/watchlist/detail.js new file mode 100644 index 0000000000000000000000000000000000000000..71a4d25cd6fa850104a300ddf893d2938534ec3d --- /dev/null +++ b/src/public/js/watchlist/detail.js @@ -0,0 +1,2 @@ +like(); +save(); \ No newline at end of file diff --git a/src/server/app/App/View.php b/src/server/app/App/View.php index b455d1d62a7dd1b8f730123aed2c83ff80d30f3b..4278869ca138a1d8064a429c8838ffcb2f749ad0 100644 --- a/src/server/app/App/View.php +++ b/src/server/app/App/View.php @@ -8,6 +8,7 @@ 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'; @@ -21,4 +22,4 @@ class View header("Location: $url"); exit(); } -} +} \ No newline at end of file diff --git a/src/server/app/Controller/BookmarkController.php b/src/server/app/Controller/BookmarkController.php new file mode 100644 index 0000000000000000000000000000000000000000..4a131ebc40d4aaf3b6a353dfa141f90ef765ee89 --- /dev/null +++ b/src/server/app/Controller/BookmarkController.php @@ -0,0 +1,72 @@ +<?php +require_once __DIR__ . '/../Config/Database.php'; +require_once __DIR__ . '/../App/View.php'; +require_once __DIR__ . '/../Exception/ValidationException.php'; + +require_once __DIR__ . '/../Repository/WatchlistSaveRepository.php'; + +require_once __DIR__ . '/../Service/BookmarkService.php'; +require_once __DIR__ . '/../Service/SessionService.php'; + +require_once __DIR__ . '/../Model/bookmark/BookmarkGetRequest.php'; + +class BookmarkController +{ + private BookmarkService $bookmarkService; + private SessionService $sessionService; + + public function __construct() + { + $connection = Database::getConnection(); + $watchlistSaveRepository = new WatchlistSaveRepository($connection); + $this->bookmarkService = new BookmarkService($watchlistSaveRepository); + + $sessionRepository = new SessionRepository($connection); + $userRepository = new UserRepository($connection); + $this->sessionService = new SessionService($sessionRepository, $userRepository); + } + + public function self() + { + $page = $_GET['page'] ?? 1; + $pageSize = $_GET['pageSize'] ?? 10; + + $user = $this->sessionService->current(); + $request = new BookmarkGetRequest(); + $request->userId = $user ? $user->id : null; + $request->page = $page; + $request->pageSize = $pageSize; + + $result = $this->bookmarkService->findByUser($request); + + function posterCompare($element1, $element2) + { + return $element1["rank"] - $element2["rank"]; + } + + $bookmarks = []; + + foreach ($result["items"] as $item) { + $posters = json_decode($item["posters"], true); + $tags = json_decode($item["tags"], true); + $tags = array_filter($tags, function ($value) { + return $value["id"] !== null; + }); + usort($posters, "posterCompare"); + $item["posters"] = $posters; + $item["tags"] = $tags; + + array_push($bookmarks, $item); + } + + $result["items"] = $bookmarks; + + View::render('profile/bookmark', [ + 'title' => 'Bookmark', + 'data' => [ + 'bookmarks' => $result, + 'userUUID' => $user ? $user->uuid : null, + ], + ], $this->sessionService); + } +} \ No newline at end of file diff --git a/src/server/app/Controller/CatalogController.php b/src/server/app/Controller/CatalogController.php index bf68439463a8608a9527840153e6b8c194f30249..c77b6a9f786b44bc56bda12287b79d1dc2c1766d 100644 --- a/src/server/app/Controller/CatalogController.php +++ b/src/server/app/Controller/CatalogController.php @@ -12,6 +12,7 @@ require_once __DIR__ . '/../Repository/UserRepository.php'; require_once __DIR__ . '/../Repository/SessionRepository.php'; require_once __DIR__ . '/../Model/CatalogCreateRequest.php'; +require_once __DIR__ . '/../Model/catalog/CatalogUpdateRequest.php'; require_once __DIR__ . '/../Model/CatalogSearchRequest.php'; class CatalogController @@ -36,16 +37,21 @@ class CatalogController $page = $_GET['page'] ?? 1; $category = $_GET['category'] ?? "MIXED"; + $user = $this->sessionService->current(); + View::render('catalog/index', [ 'title' => 'Catalog', 'styles' => [ '/css/catalog.css', ], + 'js' => [ + '/js/deleteCatalog.js' + ], 'data' => [ 'catalogs' => $this->catalogService->findAll($page, $category), - 'category' => strtoupper(trim($category)) - ], - 'is_admin' => true + 'category' => strtoupper(trim($category)), + 'userRole' => $user ? $user->role : null + ] ], $this->sessionService); } @@ -65,19 +71,17 @@ class CatalogController $catalog = $this->catalogService->findByUUID($uuid); if (!$catalog) { - View::render('catalog/not-found', [ - 'title' => 'Catalog Not Found', - 'styles' => [ - '/css/catalog-not-found.css', - ], - ], $this->sessionService); + View::redirect('/404'); } - View::render('catalog/form', [ + View::render('catalog/edit', [ 'title' => 'Edit Catalog', 'styles' => [ '/css/catalog-form.css', ], + 'js' => [ + '/js/editCatalog.js' + ], 'type' => 'edit', 'data' => $catalog->toArray() ], $this->sessionService); @@ -87,16 +91,24 @@ class CatalogController { $catalog = $this->catalogService->findByUUID($uuid); + if (!$catalog) { View::redirect('/404'); } + $user = $this->sessionService->current(); View::render('catalog/detail', [ 'title' => 'Catalog Detail', 'styles' => [ '/css/catalog-detail.css', ], - 'data' => $catalog->toArray() + 'js' => [ + '/js/deleteCatalog.js' + ], + 'data' => [ + 'item' => $catalog->toArray(), + 'userRole' => $user ? $user->role : null + ] ], $this->sessionService); } @@ -138,47 +150,6 @@ class CatalogController } } - public function postEdit($uuid): void - { - $request = new CatalogCreateRequest(); - $request->title = $_POST['title']; - $request->description = $_POST['description']; - $request->category = $_POST['category']; - - if (isset($_FILES['poster'])) { - $request->poster = $_FILES['poster']; - } - - if (isset($_FILES['trailer'])) { - $request->trailer = $_FILES['trailer']; - } - - try { - $this->catalogService->update($uuid, $request); - View::redirect('/catalog/' . $uuid); - } catch (ValidationException $exception) { - $catalog = $this->catalogService->findByUUID($uuid); - $catalog->title = $request->title; - $catalog->description = $request->description; - $catalog->category = $request->category; - View::render('catalog/form', [ - 'title' => 'Edit Catalog', - 'error' => $exception->getMessage(), - 'styles' => [ - '/css/catalog-form.css', - ], - 'type' => 'edit', - 'data' => $catalog->toArray() - ], $this->sessionService); - } - } - - public function postDelete($uuid): void - { - $this->catalogService->deleteByUUID($uuid); - View::redirect('/catalog'); - } - public function search() { $request = new CatalogSearchRequest(); @@ -197,4 +168,92 @@ class CatalogController require __DIR__ . '/../View/components/modal/watchlistAddSearchItem.php'; } } + + public function update($uuid): void + { + $user = $this->sessionService->current(); + try { + if (!$user || $user->role !== 'ADMIN') { + throw new ValidationException("You are not authorized to update this catalog."); + } + + $request = new CatalogUpdateRequest(); + + $request->uuid = $uuid; + $request->title = $_POST['title']; + $request->description = $_POST['description']; + $request->category = $_POST['category']; + + if (isset($_FILES['poster'])) { + $request->poster = $_FILES['poster']; + } + + if (isset($_FILES['trailer'])) { + $request->trailer = $_FILES['trailer']; + } + + $this->catalogService->update($request); + http_response_code(200); + $response = [ + "status" => 200, + "message" => "Successfully update catalog", + ]; + + 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); + } + } + + public function delete(string $uuid) + { + $user = $this->sessionService->current(); + + try { + if ($user && $user->role === 'ADMIN') { + $this->catalogService->deleteByUUID($uuid); + http_response_code(200); + + $response = [ + "status" => 200, + "message" => "Successfully delete catalog", + ]; + + echo json_encode($response); + } else { + throw new ValidationException("You are not authorized to delete this catalog."); + } + } 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); + } + } } \ No newline at end of file diff --git a/src/server/app/Controller/HomeController.php b/src/server/app/Controller/HomeController.php index 1e00f0fe79f48a6bb3535ca6c51e676ea98c4d40..3cbc1151903c1805988c8665c318ba0e7a6ba279 100644 --- a/src/server/app/Controller/HomeController.php +++ b/src/server/app/Controller/HomeController.php @@ -3,12 +3,14 @@ require_once __DIR__ . '/../App/View.php'; require_once __DIR__ . '/../Config/Database.php'; require_once __DIR__ . '/../Service/SessionService.php'; +require_once __DIR__ . '/../Service/TagService.php'; require_once __DIR__ . '/../Repository/UserRepository.php'; require_once __DIR__ . '/../Repository/SessionRepository.php'; require_once __DIR__ . '/../Repository/WatchlistRepository.php'; require_once __DIR__ . '/../Repository/WatchlistLikeRepository.php'; require_once __DIR__ . '/../Repository/WatchlistSaveRepository.php'; +require_once __DIR__ . '/../Repository/WatchlistTagRepository.php'; require_once __DIR__ . '/../Model/WatchlistsGetRequest.php'; @@ -16,6 +18,7 @@ class HomeController { private SessionService $sessionService; private WatchlistService $watchlistService; + private TagService $tagService; public function __construct() { @@ -29,7 +32,11 @@ class HomeController $watchlistItemRepository = new WatchlistItemRepository($connection); $watchlistLikeRepository = new WatchlistLikeRepository($connection); $watchlistSaveRepository = new WatchlistSaveRepository($connection); - $this->watchlistService = new WatchlistService($watchlistRepository, $watchlistItemRepository, $watchlistLikeRepository, $watchlistSaveRepository); + $watchlistTagRepository = new WatchlistTagRepository($connection); + $this->watchlistService = new WatchlistService($watchlistRepository, $watchlistItemRepository, $watchlistLikeRepository, $watchlistSaveRepository, $watchlistTagRepository); + + $tagRepository = new TagRepository($connection); + $this->tagService = new TagService($tagRepository); } public function index(): void @@ -83,6 +90,13 @@ class HomeController // Get current user $user = $this->sessionService->current(); + $tags = $this->tagService->findAll(); + $tagsInit = []; + + foreach ($tags["items"] as $tag) { + array_push($tagsInit, $tag->name); + } + // Get watchlists $request = new WatchlistsGetRequest(); $request->category = $_GET["category"] ?? ""; @@ -90,6 +104,8 @@ class HomeController $request->sortBy = $_GET["sortBy"] ?? ""; $request->order = $_GET["order"] ?? ""; $request->page = $_GET["page"] ?? 1; + $request->tag = $_GET["tag"] ?? ""; + $request->tagsInit = $tagsInit; $request->search = isset($_GET["search"]) ? strtolower($_GET["search"]) : ""; $request->userId = $user->id ?? -1; @@ -106,12 +122,18 @@ class HomeController "page" => $result["page"], "pageTotal" => $result["pageTotal"], "userUUID" => $user->uuid ?? "", + "tags" => $tagsInit ]; foreach ($result["items"] as $item) { $posters = json_decode($item["posters"], true); + $tags = json_decode($item["tags"], true); + $tags = array_filter($tags, function ($value) { + return $value["id"] !== null; + }); usort($posters, "posterCompare"); $item["posters"] = $posters; + $item["tags"] = $tags; array_push($data["items"], $item); } diff --git a/src/server/app/Controller/UserController.php b/src/server/app/Controller/UserController.php index 3ce5b0869ef8664efbd2b9b2cdaf0520984df2f7..54a9f7361f2e000b3a8428d449690ca104ac3609 100644 --- a/src/server/app/Controller/UserController.php +++ b/src/server/app/Controller/UserController.php @@ -120,16 +120,47 @@ class UserController public function showEditProfile(): void { - $currentUser = $this->userService->findByEmail("m17@gmail.com"); + $currentUser = $this->sessionService->current(); View::render('user/editProfile', [ 'title' => 'Drawl | Edit Profile', 'styles' => [ '/css/editProfile.css', ], + 'js' => [ + '/js/profile.js' + ], 'data' => ['name' => $currentUser->name, 'email' => $currentUser->email] ], $this->sessionService); } + public function logout(): void + { + try { + $this->sessionService->destroy(); + http_response_code(200); + $response = [ + "status" => 200, + "message" => "Logout success.", + ]; + } 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); + } + } + public function postEditProfile(): void { $request = new UserEditRequest(); @@ -138,48 +169,117 @@ class UserController $request->oldPassword = $_POST['oldPassword']; $request->newPassword = $_POST['newPassword']; - $currentUser = $this->userService->findByEmail("m17@gmail.com"); - - - if (isset($_POST['update_button'])) { - //update action - try { - $this->userService->update($currentUser, $request); - View::redirect('/editProfile'); - } catch (ValidationException $exception) { - //throw $th; - View::render('user/editProfile', [ - 'title' => 'Drawl | Edit Profile', - 'error' => $exception->getMessage(), - 'styles' => [ - '/css/editProfile.css', - ], - 'data' => [ - 'name' => $currentUser->name, - 'email' => $currentUser->email - ], - ], $this->sessionService); - } - } else if (isset($_POST['delete_button'])) { - //delete action - $this->userService->deleteBySession($currentUser->email); - $this->userService->deleteByEmail($currentUser->email); + $currentUser = $this->sessionService->current(); - $this->sessionService->destroy(); - View::redirect('/signin'); + try { + $this->userService->update($currentUser, $request); + View::redirect('/profile'); + } catch (ValidationException $exception) { + View::render('user/editProfile', [ + 'title' => 'Drawl | Edit Profile', + 'error' => $exception->getMessage(), + 'styles' => [ + '/css/editProfile.css', + ], + 'data' => [ + 'name' => $currentUser->name, + 'email' => $currentUser->email + ], + ], $this->sessionService); } } - public function deleteProfile(): void + public function update(): void { - // Get email dari $this->sessionService->current(); - $this->userService->deleteByEmail($email); - View::redirect('/signin'); + $request = new UserEditRequest(); + + $json = file_get_contents('php://input'); + $data = json_decode($json); + + if ($data === null) { + http_response_code(400); + $response = [ + "status" => 400, + "message" => "Invalid request.", + ]; + + echo json_encode($response); + return; + } + + + $request->name = $data->name; + $request->oldPassword = $data->oldPassword; + $request->newPassword = $data->newPassword; + + $currentUser = $this->sessionService->current(); + + try { + $this->userService->update($currentUser, $request); + http_response_code(200); + $response = [ + "status" => 200, + "message" => "Successfully update user", + "name" => $request->name, + ]; + + echo json_encode($response); + } catch (ValidationException $exception) { + http_response_code($exception->getCode() ?? 400); + + $response = [ + "status" => $exception->getCode() ?? 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); + } } - public function logOut(): void + public function delete(): void { - $this->sessionService->destroy(); - View::redirect("/signin"); + $currentUser = $this->sessionService->current(); + + try { + + if (!$currentUser) { + throw new ValidationException("Unauthorized.", 401); + } + $this->userService->deleteBySession($currentUser->email); + $this->userService->deleteByEmail($currentUser->email); + http_response_code(200); + + $response = [ + "status" => 200, + "message" => "Successfully delete user", + ]; + + echo json_encode($response); + } catch (ValidationException $exception) { + http_response_code($exception->getCode() ?? 400); + + $response = [ + "status" => $exception->getCode() ?? 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); + } } -} +} \ No newline at end of file diff --git a/src/server/app/Controller/WatchlistController.php b/src/server/app/Controller/WatchlistController.php index 9bc7d5498deb3c413b4857befb176dc79f5c6928..b1ae74b9011581b21467454217b6920c1ef79334 100644 --- a/src/server/app/Controller/WatchlistController.php +++ b/src/server/app/Controller/WatchlistController.php @@ -8,25 +8,30 @@ require_once __DIR__ . '/../Repository/WatchlistRepository.php'; require_once __DIR__ . '/../Repository/WatchlistItemRepository.php'; require_once __DIR__ . '/../Repository/WatchlistLikeRepository.php'; require_once __DIR__ . '/../Repository/WatchlistSaveRepository.php'; +require_once __DIR__ . '/../Repository/TagRepository.php'; +require_once __DIR__ . '/../Repository/WatchlistTagRepository.php'; require_once __DIR__ . '/../Service/CatalogService.php'; require_once __DIR__ . '/../Service/WatchlistService.php'; require_once __DIR__ . '/../Service/SessionService.php'; +require_once __DIR__ . '/../Service/TagService.php'; require_once __DIR__ . '/../Model/CatalogCreateRequest.php'; require_once __DIR__ . '/../Model/WatchlistAddItemRequest.php'; require_once __DIR__ . '/../Model/WatchlistCreateRequest.php'; -require_once __DIR__ . '/../Model/watchlist/WatchlistGetSelfRequest.php'; +require_once __DIR__ . '/../Model/watchlist/WatchlistGetOneByUserRequest.php'; require_once __DIR__ . '/../Model/watchlist/WatchlistGetOneRequest.php'; require_once __DIR__ . '/../Model/watchlist/WatchlistLikeRequest.php'; require_once __DIR__ . '/../Model/watchlist/WatchlistSaveRequest.php'; require_once __DIR__ . '/../Model/watchlist/WatchlistEditRequest.php'; +require_once __DIR__ . '/../Model/watchlist/WatchlistDeleteRequest.php'; class WatchlistController { private CatalogService $catalogService; private WatchlistService $watchlistService; private SessionService $sessionService; + private TagService $tagService; public function __construct() { @@ -38,18 +43,27 @@ class WatchlistController $watchlistItemRepository = new WatchlistItemRepository($connection); $watchlistLikeRepository = new WatchlistLikeRepository($connection); $watchlistSaveRepository = new WatchlistSaveRepository($connection); - $this->watchlistService = new WatchlistService($watchlistRepository, $watchlistItemRepository, $watchlistLikeRepository, $watchlistSaveRepository); + $watchlistTagRepository = new WatchlistTagRepository($connection); + $this->watchlistService = new WatchlistService($watchlistRepository, $watchlistItemRepository, $watchlistLikeRepository, $watchlistSaveRepository, $watchlistTagRepository); $sessionRepository = new SessionRepository($connection); $userRepository = new UserRepository($connection); $this->sessionService = new SessionService($sessionRepository, $userRepository); + + $tagRepository = new TagRepository($connection); + $this->tagService = new TagService($tagRepository); } public function create(): void { + $tags = $this->tagService->findAll(); + View::render('watchlist/createUpdate', [ 'title' => 'Create Watchlist', 'description' => 'Create new watchlist', + "data" => [ + "tags" => $tags["items"], + ], 'styles' => [ '/css/watchlistCreate.css', '/css/components/watchlist/watchlistItem.css', @@ -72,12 +86,16 @@ class WatchlistController $dataRaw = file_get_contents("php://input"); $data = json_decode($dataRaw, true); + $tags = $this->tagService->findAll(); + $request = new WatchlistCreateRequest(); $request->title = $data["title"]; $request->description = $data["description"]; $request->visibility = $data["visibility"]; $request->items = $data["items"]; + $request->tags = $data["tags"]; $request->userId = $user->id; + $request->initialTags = $tags["items"]; try { $this->watchlistService->create($request); @@ -85,7 +103,7 @@ class WatchlistController $response = [ "status" => 200, "message" => "Watchlist successfully created", - "redirectTo" => "/", + "redirectTo" => "/profile/watchlist", ]; print_r(json_encode($response)); @@ -94,7 +112,7 @@ class WatchlistController $response = [ "status" => 500, - "message" => "Internal server error. Please try again later." + "message" => $exception->getMessage() ]; print_r(json_encode($response)); @@ -105,6 +123,8 @@ class WatchlistController { $user = $this->sessionService->current(); + $tags = $this->tagService->findAll(); + $getRequest = new WatchlistsGetOneRequest(); $getRequest->uuid = $uuid; $getRequest->page = 1; @@ -124,7 +144,9 @@ class WatchlistController "title" => $watchlist["title"], "description" => $watchlist["description"], "visibility" => $watchlist["visibility"], - "catalogs" => $watchlist["catalogs"] + "catalogs" => $watchlist["catalogs"], + "tagsSelected" => $watchlist["tags"], + "tags" => $tags["items"], ], 'styles' => [ '/css/watchlistCreate.css', @@ -166,6 +188,8 @@ class WatchlistController print_r(json_encode($response)); } + $tags = $this->tagService->findAll(); + $request = new WatchlistEditRequest(); $request->watchlist = $watchlist; $request->userId = $user->id; @@ -173,14 +197,16 @@ class WatchlistController $request->description = $data["description"]; $request->visibility = $data["visibility"]; $request->items = $data["items"]; + $request->tags = $data["tags"]; + $request->initialTags = $tags["items"]; try { $this->watchlistService->edit($request); $response = [ "status" => 200, - "message" => "Success", - "redirectTo" => "", + "message" => "Watchlist edited successfully", + "redirectTo" => "/watchlist/{$watchlist["watchlist_uuid"]}", ]; print_r(json_encode($response)); @@ -198,21 +224,30 @@ class WatchlistController public function detail(string $uuid): void { + $user = $this->sessionService->current(); $request = new WatchlistsGetOneRequest(); $request->uuid = $uuid; $request->page = $_GET["page"] ?? 1; + $request->userId = $user ? $user->id : -1; $result = $this->watchlistService->findByUUID($request); if ($result == null) { View::redirect('/404'); } + View::render('watchlist/detail', [ 'title' => 'Watchlist', 'styles' => [ '/css/watchlist-detail.css', ], - 'data' => $result, - 'editable' => true, + 'js' => [ + '/js/watchlist/detail.js', + '/js/watchlist/delete.js', + ], + 'data' => [ + 'item' => $result, + 'userUUID' => $user ? $user->uuid : null + ] ], $this->sessionService); } @@ -236,10 +271,11 @@ class WatchlistController { $user = $this->sessionService->current(); - $request = new WatchlistsGetSelfRequest(); + $request = new WatchlistGetOneByUserRequest(); $request->visibility = $_GET["visibility"] ?? ""; + $request->userId = $user->id; - $result = $this->watchlistService->findUserBookmarks($request); + $result = $this->watchlistService->findByUser($request); function posterCompare($element1, $element2) { @@ -250,15 +286,20 @@ class WatchlistController foreach ($result["items"] as $item) { $posters = json_decode($item["posters"], true); + $tags = json_decode($item["tags"], true); + $tags = array_filter($tags, function ($value) { + return $value["id"] !== null; + }); usort($posters, "posterCompare"); $item["posters"] = $posters; + $item["tags"] = $tags; array_push($watchlists, $item); } $result["items"] = $watchlists; - View::render('watchlist/self', [ + View::render('profile/watchlist', [ 'title' => 'My Watchlist', 'description' => 'My watchlist', 'styles' => [ @@ -281,8 +322,7 @@ class WatchlistController $response = [ "message" => "Please login before liking this watchlist.", ]; - $response = json_encode($response); - echo $response; + print_r(json_encode($response)); return; } @@ -295,6 +335,13 @@ class WatchlistController try { $this->watchlistService->like($watchlistLikeRequest); + + $response = [ + "status" => 200, + "message" => "Success", + ]; + + print_r(json_encode($response)); } catch (ValidationException $exception) { http_response_code(500); @@ -303,7 +350,7 @@ class WatchlistController "message" => $exception->getMessage(), ]; - echo json_encode($response); + print_r(json_encode($response)); } } @@ -321,6 +368,13 @@ class WatchlistController try { $this->watchlistService->bookmark($watchlistSaveRequest); + + $response = [ + "status" => 200, + "message" => "Success", + ]; + + print_r(json_encode($response)); } catch (ValidationException $exception) { http_response_code(500); @@ -329,7 +383,57 @@ class WatchlistController "message" => $exception->getMessage(), ]; - echo json_encode($response); + print_r(json_encode($response)); + } + } + + public function delete() + { + $user = $this->sessionService->current(); + + $dataRaw = file_get_contents("php://input"); + $data = json_decode($dataRaw, true); + + $getRequest = new WatchlistsGetOneRequest(); + $getRequest->uuid = $data["watchlistUUID"]; + $getRequest->page = 1; + $getRequest->pageSize = 100; + + $watchlist = $this->watchlistService->findByUUID($getRequest); + + if ($watchlist == null || $user->uuid !== $watchlist["creator_uuid"]) { + http_response_code(400); + + $response = [ + "status" => 400, + "message" => "Watchlist not found.", + ]; + + print_r(json_encode($response)); + } + + $request = new WatchlistDeleteRequest(); + $request->watchlistUUID = $data["watchlistUUID"]; + + try { + $this->watchlistService->deleteByUUID($request); + + $response = [ + "status" => 200, + "message" => "Watchlist deleted successfully", + "redirectTo" => "/profile/watchlist", + ]; + + print_r(json_encode($response)); + } catch (Exception $exception) { + http_response_code(500); + + $response = [ + "status" => 500, + "message" => "Failed to delete watchlist. " . $exception->getMessage(), + ]; + + print_r(json_encode($response)); } } } \ No newline at end of file diff --git a/src/server/app/Domain/Tag.php b/src/server/app/Domain/Tag.php new file mode 100644 index 0000000000000000000000000000000000000000..f48a5e42741c754d928de95c2d5eb527aa9ac221 --- /dev/null +++ b/src/server/app/Domain/Tag.php @@ -0,0 +1,42 @@ +<?php + +require_once __DIR__ . '/../App/Domain.php'; + +class Tag extends Domain +{ + public int|string $id; + public string $name; + public string $createdAt; + + public function toArray(): array + { + $array = [ + "name" => $this->name, + ]; + + if (isset($this->id)) { + $array["id"] = $this->id; + } + + if (isset($this->createdAt)) { + $array["created_at"] = $this->createdAt; + } + + return $array; + } + + public function fromArray(array $data) + { + if (isset($data["id"])) { + $this->id = $data["id"]; + } + + if (isset($data["name"])) { + $this->name = $data["name"]; + } + + if (isset($data["created_at"])) { + $this->createdAt = $data["created_at"]; + } + } +} \ No newline at end of file diff --git a/src/server/app/Domain/WatchlistSave.php b/src/server/app/Domain/WatchlistSave.php new file mode 100644 index 0000000000000000000000000000000000000000..4a631e6217e71cd6a4159df6c2b74c71fa1f5933 --- /dev/null +++ b/src/server/app/Domain/WatchlistSave.php @@ -0,0 +1,39 @@ +<?php + +require_once __DIR__ . '/../App/Domain.php'; + +class WatchlistSave extends Domain +{ + public int|string $id; + public int $user_id; + public int $watchlist_id; + + public function toArray(): array + { + $array = [ + 'user_id' => $this->user_id, + 'watchlist_id' => $this->watchlist_id, + ]; + + if (isset($this->id)) { + $array['id'] = $this->id; + } + + return $array; + } + + public function fromArray(array $data) + { + if (isset($data["id"])) { + $this->id = $data["id"]; + } + + if (isset($data["user_id"])) { + $this->user_id = $data["user_id"]; + } + + if (isset($data["watchlist_id"])) { + $this->watchlist_id = $data["watchlist_id"]; + } + } +} \ No newline at end of file diff --git a/src/server/app/Domain/WatchlistTag.php b/src/server/app/Domain/WatchlistTag.php new file mode 100644 index 0000000000000000000000000000000000000000..989ddda1e3fb28227d46de65b9b08e55c67840f2 --- /dev/null +++ b/src/server/app/Domain/WatchlistTag.php @@ -0,0 +1,39 @@ +<?php + +require_once __DIR__ . '/../App/Domain.php'; + +class WatchlistTag extends Domain +{ + public int|string $id; + public int $tagId; + public int $watchlistId; + + public function toArray(): array + { + $array = [ + "tag_id" => $this->tagId, + "watchlist_id" => $this->watchlistId + ]; + + if (isset($this->id)) { + $array["id"] = $this->id; + } + + return $array; + } + + public function fromArray(array $data) + { + if (isset($data["id"])) { + $this->id = $data["id"]; + } + + if (isset($data["tag_id"])) { + $this->tagId = $data["tag_id"]; + } + + if (isset($data["watchlist_id"])) { + $this->watchlistId = $data["watchlist_id"]; + } + } +} \ No newline at end of file diff --git a/src/server/app/Exception/ValidationException.php b/src/server/app/Exception/ValidationException.php index 49b82c1e3ba3da127fa2522c6e783d47819bde89..94f0c43539b3f2327b041a5a980bef130da92745 100644 --- a/src/server/app/Exception/ValidationException.php +++ b/src/server/app/Exception/ValidationException.php @@ -2,5 +2,12 @@ class ValidationException extends \Exception { - -} + public function __construct( + $message, + int|null $code = 0, + Throwable|null $previous = null + ) { + parent::__construct($message, $code, $previous); + $this->code = 400; + } +} \ No newline at end of file diff --git a/src/server/app/Model/CatalogCreateRequest.php b/src/server/app/Model/CatalogCreateRequest.php index 9ceb0f3fec592f76e970f195c11980c4414cf624..13ccdaaa0c8144b8a6bbf65bf64d56bee001b159 100644 --- a/src/server/app/Model/CatalogCreateRequest.php +++ b/src/server/app/Model/CatalogCreateRequest.php @@ -2,9 +2,9 @@ class CatalogCreateRequest { - public ?string $title = null; - public ?string $description = null; + public string $title; + public string $description; public $poster = null; public $trailer = null; - public ?string $category = null; + public string $category; } \ No newline at end of file diff --git a/src/server/app/Model/WatchlistCreateRequest.php b/src/server/app/Model/WatchlistCreateRequest.php index 35bda1139f54c8c9bfbe588a93b5174138397c7a..66e93e891de398931dec9805c6f7427ee3aacb03 100644 --- a/src/server/app/Model/WatchlistCreateRequest.php +++ b/src/server/app/Model/WatchlistCreateRequest.php @@ -7,4 +7,6 @@ class WatchlistCreateRequest public ?string $description; public ?string $visibility; public ?array $items; + public ?array $tags; + public ?array $initialTags; } diff --git a/src/server/app/Model/WatchlistsGetRequest.php b/src/server/app/Model/WatchlistsGetRequest.php index f2f301b5e8d02ad740ba33b2db2181e0966ec0be..47471033e758e583a56fbf2af3a8316c9bb77023 100644 --- a/src/server/app/Model/WatchlistsGetRequest.php +++ b/src/server/app/Model/WatchlistsGetRequest.php @@ -5,7 +5,8 @@ class WatchlistsGetRequest public ?int $userId; public ?string $search; public ?string $category; - public ?string $tags; + public ?string $tag; + public ?array $tagsInit; public ?string $sortBy; public ?string $order; public ?int $page; diff --git a/src/server/app/Model/bookmark/BookmarkGetRequest.php b/src/server/app/Model/bookmark/BookmarkGetRequest.php new file mode 100644 index 0000000000000000000000000000000000000000..971d81536ca19d4216a6fe01d869c3cc31370aad --- /dev/null +++ b/src/server/app/Model/bookmark/BookmarkGetRequest.php @@ -0,0 +1,8 @@ +<?php + +class BookmarkGetRequest +{ + public int $userId; + public int $page = 1; + public int $pageSize = 10; +} \ No newline at end of file diff --git a/src/server/app/Model/catalog/CatalogUpdateRequest.php b/src/server/app/Model/catalog/CatalogUpdateRequest.php new file mode 100644 index 0000000000000000000000000000000000000000..2f07d18d5988950bfca1742ce1d20eb4ad253b92 --- /dev/null +++ b/src/server/app/Model/catalog/CatalogUpdateRequest.php @@ -0,0 +1,11 @@ +<?php + +class CatalogUpdateRequest +{ + public string $uuid; + public string $title; + public string $description; + public $poster = null; + public $trailer = null; + public string $category; +} \ No newline at end of file diff --git a/src/server/app/Model/watchlist/WatchlistDeleteRequest.php b/src/server/app/Model/watchlist/WatchlistDeleteRequest.php new file mode 100644 index 0000000000000000000000000000000000000000..723cbf52bf9198c5097c42a4165f50013be89b73 --- /dev/null +++ b/src/server/app/Model/watchlist/WatchlistDeleteRequest.php @@ -0,0 +1,6 @@ +<?php + +class WatchlistDeleteRequest +{ + public ?string $watchlistUUID; +} \ No newline at end of file diff --git a/src/server/app/Model/watchlist/WatchlistEditRequest.php b/src/server/app/Model/watchlist/WatchlistEditRequest.php index 3e7feadb95d300a9978a0accde0a8cf809b448c8..d5caaeddc2fc321a6716087a800d84955f05d52e 100644 --- a/src/server/app/Model/watchlist/WatchlistEditRequest.php +++ b/src/server/app/Model/watchlist/WatchlistEditRequest.php @@ -8,4 +8,6 @@ class WatchlistEditRequest public ?string $description; public ?string $visibility; public ?array $items; + public ?array $tags; + public ?array $initialTags; } \ No newline at end of file diff --git a/src/server/app/Model/watchlist/WatchlistGetOneByUserRequest.php b/src/server/app/Model/watchlist/WatchlistGetOneByUserRequest.php new file mode 100644 index 0000000000000000000000000000000000000000..908494ee4f54842871813c499948d383d8a8a69d --- /dev/null +++ b/src/server/app/Model/watchlist/WatchlistGetOneByUserRequest.php @@ -0,0 +1,7 @@ +<?php + +class WatchlistGetOneByUserRequest +{ + public int $userId; + public ?string $visibility; +} \ No newline at end of file diff --git a/src/server/app/Model/watchlist/WatchlistGetOneRequest.php b/src/server/app/Model/watchlist/WatchlistGetOneRequest.php index 50c9200d60e181f10b0d8766e80863d9bc7f3b49..15e04b8876d921294b5069915ea59fd9bddb695f 100644 --- a/src/server/app/Model/watchlist/WatchlistGetOneRequest.php +++ b/src/server/app/Model/watchlist/WatchlistGetOneRequest.php @@ -2,6 +2,7 @@ class WatchlistsGetOneRequest { + public ?int $userId = null; public string $uuid; public int $page = 1; public int $pageSize = 10; diff --git a/src/server/app/Model/watchlist/WatchlistGetSelfRequest.php b/src/server/app/Model/watchlist/WatchlistGetSelfRequest.php deleted file mode 100644 index b51548056328ce95d00f6bde63c01443f02d6e1f..0000000000000000000000000000000000000000 --- a/src/server/app/Model/watchlist/WatchlistGetSelfRequest.php +++ /dev/null @@ -1,6 +0,0 @@ -<?php - -class WatchlistsGetSelfRequest -{ - public ?string $visibility; -} \ No newline at end of file diff --git a/src/server/app/Repository/TagRepository.php b/src/server/app/Repository/TagRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..b2be3393966503e918e8664e1690eb3e1628867f --- /dev/null +++ b/src/server/app/Repository/TagRepository.php @@ -0,0 +1,44 @@ +<?php + +require_once __DIR__ . '/../App/Repository.php'; + +require_once __DIR__ . '/../Domain/Tag.php'; + +class TagRepository extends Repository +{ + protected string $table = "tags"; + + public function __construct(PDO $connection) + { + parent::__construct($connection); + } + + public function findAll(array $projection = [], int|null $page = null, int|null $pageSize = null): array + { + $result = parent::findAll($projection, $page, $pageSize); + + $result['items'] = array_map( + function ($row) { + $tags = new Tag(); + $tags->fromArray($row); + return $tags; + }, + $result['items'] + ); + return $result; + } + + public function findOne($key, $value, $projection = []) + { + $result = parent::findOne($key, $value, $projection); + + if ($result != null) { + $user = new User(); + $user->fromArray($result); + + return $user; + } else { + return null; + } + } +} \ No newline at end of file diff --git a/src/server/app/Repository/UserRepository.php b/src/server/app/Repository/UserRepository.php index 507209d52219c98a089d300a7bc579320cf2c6a1..a29dc04a6e938c8eb7dd407a9a17860d6088284e 100644 --- a/src/server/app/Repository/UserRepository.php +++ b/src/server/app/Repository/UserRepository.php @@ -48,70 +48,7 @@ class UserRepository extends Repository return $user; } - // Bisa lansung pakai method findOne - public function findById(int $id): ?User - { - $statement = $this->connection->prepare("SELECT id, name, password, email, role FROM users WHERE id = ?"); - $statement->execute([$id]); - - try { - if ($row = $statement->fetch()) { - $user = new User(); - $user->id = $row['id']; - $user->name = $row['name']; - $user->password = $row['password']; - $user->email = $row['email']; - $user->role = $row['role']; - - return $user; - } else { - return null; - } - } finally { - $statement->closeCursor(); - } - } - - // Bisa lansung pakai method findOne - public function findByEmail(string $email): ?User - { - $statement = $this->connection->prepare("SELECT id, name, password, email, role FROM users WHERE email = ?"); - $statement->execute([$email]); - - try { - if ($row = $statement->fetch()) { - $user = new User(); - $user->id = $row['id']; - $user->name = $row['name']; - $user->password = $row['password']; - $user->email = $row['email']; - $user->role = $row['role']; - - return $user; - } else { - return null; - } - } finally { - $statement->closeCursor(); - } - } - - // Bisa lansung pakai method delete yang ada di repository - public function deleteByUUID(string $UUID): void - { - $statement = $this->connection->prepare("DELETE FROM users WHERE uuid = ?"); - $statement->execute([$UUID]); - $statement->closeCursor(); - } - - public function deleteByEmail(string $email): void - { - $statement = $this->connection->prepare("DELETE FROM users WHERE email = ?"); - $statement->execute([$email]); - $statement->closeCursor(); - } - // Bisa langsung pakai method delete yang ada di repository public function deleteBySession(string $email) { $statement = $this->connection->prepare("DELETE FROM sessions WHERE user_id IN diff --git a/src/server/app/Repository/WatchlistLikeRepository.php b/src/server/app/Repository/WatchlistLikeRepository.php index 8cef6e2b08686edca6223b86330721314c0f29c5..fab36084bc8a1d82b7808de292401f2abca292dc 100644 --- a/src/server/app/Repository/WatchlistLikeRepository.php +++ b/src/server/app/Repository/WatchlistLikeRepository.php @@ -1,5 +1,7 @@ <?php +require_once __DIR__ . '/../App/Repository.php'; + class WatchlistLikeRepository extends Repository { protected string $table = "watchlist_like"; diff --git a/src/server/app/Repository/WatchlistRepository.php b/src/server/app/Repository/WatchlistRepository.php index 1b4844e8423591919ac4edf0e1f3db0e9fbbfe76..8245c38a7d128eb7b1ea436aa45133ab3624ff3d 100644 --- a/src/server/app/Repository/WatchlistRepository.php +++ b/src/server/app/Repository/WatchlistRepository.php @@ -42,19 +42,36 @@ class WatchlistRepository extends Repository } } - public function findAllCustom(string $userId, string $search, string $category, string $sortBy, string $order, int $page = 1, int $pageSize = 10) + public function findAllCustom(string $userId, string $search, string $category, string $sortBy, string $order, string $tag, int $page = 1, int $pageSize = 10) { if ($category != "") $category = " AND category = '$category'"; // Queries - $selectQuery = " - SELECT w.id AS watchlist_id, json_agg(json_build_object( + $selectFirstQuery = " + WITH first_agg AS ( + SELECT w.id AS watchlist_id, jsonb_agg(jsonb_build_object( 'rank', rank, 'poster', poster, 'catalog_uuid', c.uuid - )) AS posters, w.uuid AS watchlist_uuid, name AS creator, u.uuid AS creator_uuid, item_count, loved, saved, w.title, w.description, w.category, visibility, love_count, w.created_at AS created_at + )) AS posters, w.uuid AS watchlist_uuid, u.name AS creator, u.uuid AS creator_uuid, item_count, loved, saved, w.title, w.description, w.category, visibility, love_count, w.created_at AS created_at "; + $selectSecondQuery = " + ) + SELECT + jsonb_agg(jsonb_build_object( + 'id', t.id, + 'name', t.name + )) AS tags, w.watchlist_id, w.posters as posters, w.watchlist_uuid, w.creator, w.creator_uuid, w.item_count, w.loved, w.saved, w.title, w.description, w.category, w.visibility, w.love_count, w.created_at + FROM first_agg AS w + LEFT JOIN watchlist_tag as wt ON wt.watchlist_id = w.watchlist_id + LEFT JOIN tags as t ON t.id = wt.tag_id + GROUP BY + w.watchlist_id, w.watchlist_uuid, w.posters, w.creator, w.creator_uuid, w.item_count, w.loved, w.saved, w.title, w.description, w.category, w.visibility, w.love_count, w.created_at + ORDER BY + $sortBy $order, + w.created_at DESC + "; $countQuery = "WITH rows AS (SELECT COUNT(*)"; $mainQuery = " FROM ( @@ -95,16 +112,39 @@ class WatchlistRepository extends Repository u.name ILIKE :creator OR w.title ILIKE :watchlist_title GROUP BY - w.id, w.uuid, u.name, w.title, name, u.uuid, item_count, loved, saved, w.description, w.category, visibility, love_count, w.created_at + w.id, w.uuid, u.name, w.title, u.name, u.uuid, item_count, loved, saved, w.description, w.category, visibility, love_count, w.created_at ORDER BY $sortBy $order, w.created_at DESC - LIMIT :limit - OFFSET :offset "; - $selectStatement = $this->connection->prepare($selectQuery . $mainQuery); - $pageCountStatement = $this->connection->prepare($countQuery . $mainQuery . ") SELECT COUNT(*) FROM rows"); + if ($tag) { + $selectStatement = $this->connection->prepare("WITH outer_query AS (" . + $selectFirstQuery . $mainQuery . $selectSecondQuery . + ") SELECT * FROM outer_query as o + WHERE EXISTS + (SELECT 1 + FROM jsonb_array_elements(o.tags) + AS elem WHERE elem @> '{\"name\": \"$tag\"}'::jsonb) + LIMIT :limit + OFFSET :offset + "); + $pageCountStatement = $this->connection->prepare("WITH outer_query AS (" . + $selectFirstQuery . $mainQuery . $selectSecondQuery . + ") SELECT COUNT(*) FROM outer_query as o + WHERE EXISTS + (SELECT 1 + FROM jsonb_array_elements(o.tags) + AS elem WHERE elem @> '{\"name\": \"$tag\"}'::jsonb) + LIMIT :limit + OFFSET :offset + "); + } else { + $selectStatement = $this->connection->prepare($selectFirstQuery . $mainQuery . $selectSecondQuery . " LIMIT :limit + OFFSET :offset"); + $pageCountStatement = $this->connection->prepare($countQuery . $mainQuery . ") SELECT COUNT(*) FROM rows"); + } + // Binding select $selectStatement->bindValue(":user_id", $userId); @@ -116,8 +156,10 @@ class WatchlistRepository extends Repository // Binding count $pageCountStatement->bindValue(":user_id", $userId); $pageCountStatement->bindValue(":watchlist_title", '%' . $search . '%'); - $pageCountStatement->bindValue(":limit", PHP_INT_MAX); - $pageCountStatement->bindValue(":offset", 0); + if ($tag) { + $pageCountStatement->bindValue(":limit", PHP_INT_MAX); + $pageCountStatement->bindValue(":offset", 0); + } $pageCountStatement->bindValue(":creator", '%' . $search . '%'); $selectStatement->execute(); @@ -135,7 +177,7 @@ class WatchlistRepository extends Repository } } - public function findUserBookmarks(int $userId, string|null $visibility, int $page = null, int $pageSize = null) + public function findByUser(int $userId, string|null $visibility, int $page = null, int $pageSize = null) { $query = " FROM ( @@ -148,7 +190,7 @@ class WatchlistRepository extends Repository WHERE user_id = :user_id ) THEN TRUE ELSE FALSE - END AS like_status + END AS liked FROM watchlists WHERE @@ -159,18 +201,32 @@ class WatchlistRepository extends Repository JOIN (SELECT * FROM watchlist_items WHERE rank < 5) AS wi ON wi.watchlist_id = w.id JOIN catalogs AS c ON c.id = wi.catalog_id "; - - - $selectQuery = "SELECT w.id AS watchlist_id, json_agg(json_build_object( + $selectFirstQuery = " + WITH first_agg AS ( + SELECT w.id AS watchlist_id, jsonb_agg(jsonb_build_object( 'rank', rank, 'poster', poster, 'catalog_uuid', c.uuid - )) AS posters, w.uuid AS watchlist_uuid, name AS creator, u.uuid AS creator_uuid, item_count, like_status, w.title, w.description, w.category, visibility, like_count, w.updated_at AS updated_at, w.created_at AS created_at "; - + )) AS posters, w.uuid AS watchlist_uuid, name AS creator, u.uuid AS creator_uuid, item_count, liked, w.title, w.description, w.category, visibility, like_count, w.updated_at AS updated_at, w.created_at AS created_at"; + $selectSecondQuery = " + ) + SELECT + jsonb_agg(jsonb_build_object( + 'id', t.id, + 'name', t.name + )) AS tags, fa.watchlist_id, fa.posters as posters, fa.watchlist_uuid, fa.creator, fa.creator_uuid, fa.item_count, fa.liked, fa.title, fa.description, fa.category, fa.visibility, fa.like_count, fa.created_at + FROM first_agg AS fa + LEFT JOIN watchlist_tag as wt ON wt.watchlist_id = fa.watchlist_id + LEFT JOIN tags as t ON t.id = wt.tag_id + GROUP BY + fa.watchlist_id, fa.watchlist_uuid, fa.posters, fa.creator, fa.creator_uuid, fa.item_count, fa.liked, fa.title, fa.description, fa.category, fa.visibility, fa.like_count, fa.created_at + ORDER BY + fa.created_at DESC + "; $pageCountQuery = "SELECT COUNT(*) "; - $selectStatement = $this->connection->prepare($selectQuery . $query . "GROUP BY - watchlist_id, watchlist_uuid, creator, u.uuid, w.title, w.uuid, name, item_count, like_status, w.id, w.description, w.category, visibility, like_count, w.updated_at, w.created_at;"); + $selectStatement = $this->connection->prepare($selectFirstQuery . $query . "GROUP BY + watchlist_id, watchlist_uuid, creator, u.uuid, w.title, w.uuid, u.name, item_count, liked, w.id, w.description, w.category, visibility, like_count, w.updated_at, w.created_at" . $selectSecondQuery); $selectStatement->bindValue(':user_id', $userId, PDO::PARAM_INT); $selectStatement->bindValue(':limit', $pageSize, PDO::PARAM_INT); $offset = ($page - 1) * $pageSize; @@ -204,17 +260,18 @@ class WatchlistRepository extends Repository public function findByUUID(string $uuid, int|null $user_id, int $page = 1, int $pageSize = 10) { $selectQuery = " + WITH first_agg AS ( WITH w AS ( SELECT - id, uuid, title, description, category, visibility, like_count, item_count, user_id, updated_at " . - ($user_id === null ? "" : ", CASE + id, uuid, title, description, category, visibility, like_count, item_count, user_id, created_at + ,CASE WHEN id IN ( SELECT watchlist_id FROM watchlist_like WHERE user_id = :user_id ) THEN TRUE ELSE FALSE - END AS like_status, + END AS liked, CASE WHEN id IN ( SELECT watchlist_id @@ -222,13 +279,14 @@ class WatchlistRepository extends Repository WHERE user_id = :user_id ) THEN TRUE ELSE FALSE - END AS save_status ") . " + END AS saved FROM watchlists WHERE watchlists.uuid = :uuid - LIMIT 1) - SELECT w.id AS watchlist_id, json_agg(json_build_object( + LIMIT 1 + ) + SELECT w.id AS watchlist_id, jsonb_agg(jsonb_build_object( 'rank', rank, 'poster', poster, 'catalog_uuid', c.uuid, @@ -236,14 +294,28 @@ class WatchlistRepository extends Repository 'description', wi.description, 'title', c.title, 'category', c.category - )) AS catalogs, w.uuid AS watchlist_uuid, name AS creator, item_count, w.title, w.description, w.category, visibility, like_count, w.updated_at AS updated_at, u.uuid AS creator_uuid" - . ($user_id === null ? "" : ", like_status, save_status") . " + )) AS catalogs, w.uuid AS watchlist_uuid, name AS creator, item_count, w.title, w.description, w.category, visibility, like_count, w.created_at, u.uuid AS creator_uuid + ,liked, saved FROM w JOIN users AS u ON w.user_id = u.id , (SELECT * FROM watchlist_items WHERE watchlist_id IN (SELECT id FROM w) ORDER BY rank LIMIT :limit OFFSET :offset) AS wi JOIN catalogs AS c ON c.id = wi.catalog_id GROUP BY - watchlist_id, watchlist_uuid, creator, w.title, w.uuid, name, item_count, w.id, w.description, w.category, visibility, like_count, w.updated_at, u.uuid" - . ($user_id === null ? "" : ", like_status, save_status "); + watchlist_id, watchlist_uuid, creator, w.title, w.uuid, name, item_count, w.id, w.description, w.category, visibility, like_count, w.created_at, u.uuid + ,liked, saved + ) + SELECT + jsonb_agg(jsonb_build_object( + 'id', t.id, + 'name', t.name + )) AS tags, fa.watchlist_id, fa.catalogs, fa.watchlist_uuid, fa.creator, fa.creator_uuid, fa.item_count, fa.liked, fa.saved, fa.title, fa.description, fa.category, fa.visibility, fa.like_count, fa.created_at + FROM first_agg AS fa + LEFT JOIN watchlist_tag as wt ON wt.watchlist_id = fa.watchlist_id + LEFT JOIN tags as t ON t.id = wt.tag_id + GROUP BY + fa.watchlist_id, fa.watchlist_uuid, fa.catalogs, fa.creator, fa.creator_uuid, fa.item_count, fa.liked, fa.saved, fa.title, fa.description, fa.category, fa.visibility, fa.like_count, fa.created_at + ORDER BY + fa.created_at DESC + "; $pageCountQuery = " SELECT COUNT(*) @@ -258,21 +330,16 @@ class WatchlistRepository extends Repository JOIN catalogs AS c ON c.id = wi.catalog_id "; - $selectStatement = $this->connection->prepare($selectQuery); - $selectStatement->bindValue(':uuid', $uuid, PDO::PARAM_STR); - $selectStatement->bindValue(':limit', $pageSize, PDO::PARAM_INT); + $selectStatement->bindParam(':uuid', $uuid, PDO::PARAM_STR); + $selectStatement->bindParam(':limit', $pageSize, PDO::PARAM_INT); $offset = ($page - 1) * $pageSize; - $selectStatement->bindValue(':offset', $offset, PDO::PARAM_INT); - if (!empty($user_id)) { - $selectStatement->bindValue(':user_id', $user_id, PDO::PARAM_INT); - } + $selectStatement->bindParam(':offset', $offset, PDO::PARAM_INT); + $selectStatement->bindParam(':user_id', $user_id, PDO::PARAM_INT); + $pageCountStatement = $this->connection->prepare($pageCountQuery); - $pageCountStatement->bindValue(':uuid', $uuid, PDO::PARAM_STR); - if (!empty($user_id)) { - $pageCountStatement->bindValue(':user_id', $user_id, PDO::PARAM_INT); - } + $pageCountStatement->bindParam(':uuid', $uuid, PDO::PARAM_STR); $selectStatement->execute(); $pageCountStatement->execute(); @@ -286,7 +353,12 @@ class WatchlistRepository extends Repository $totalPage = $pageSize > 0 ? ceil($pageCountStatement->fetchColumn() / $pageSize) : 1; if ($rows = $selectStatement->fetch()) { $catalogs = json_decode($rows["catalogs"], true); + $tags = json_decode($rows["tags"], true); + $tags = array_filter($tags, function ($value) { + return $value["id"] !== null; + }); usort($catalogs, "catalogCompare"); + $rows["tags"] = $tags; $rows["catalogs"] = [ "items" => $catalogs, diff --git a/src/server/app/Repository/WatchlistSaveRepository.php b/src/server/app/Repository/WatchlistSaveRepository.php index 40df0057f25450503706a051e328ccd2418ea5e8..cc9778ade84217ec89b3a53ab25c03da41471059 100644 --- a/src/server/app/Repository/WatchlistSaveRepository.php +++ b/src/server/app/Repository/WatchlistSaveRepository.php @@ -9,6 +9,108 @@ class WatchlistSaveRepository extends Repository parent::__construct($connection); } + public function findByUser(string $userId, int $page, int $pageSize) + { + $selectQuery = " + WITH first_agg AS ( + SELECT + w.id AS watchlist_id, + jsonb_agg( + jsonb_build_object( + 'rank', rank, + 'poster', poster, + 'catalog_uuid', c.uuid + ) + ) AS posters, + w.uuid AS watchlist_uuid, name AS creator, u.uuid AS creator_uuid, item_count, + liked, w.title, w.description, w.category, visibility, + like_count, w.created_at AS created_at, w.updated_at AS updated_at + FROM + ( + SELECT + w.id, uuid, title, description, category, + visibility, like_count, item_count, w.user_id, + created_at, updated_at, + CASE WHEN w.id IN( + SELECT watchlist_id + FROM watchlist_like + WHERE user_id = :user_id + ) THEN TRUE ELSE FALSE + END AS liked + FROM watchlists w + JOIN watchlist_save wv ON + w.id = wv.watchlist_id + WHERE wv.user_id = :user_id + LIMIT :limit + OFFSET :offset + ) AS w + JOIN users AS u ON + w.user_id = u.id + JOIN( + SELECT * + FROM watchlist_items + WHERE rank < 5 + ) AS wi + ON wi.watchlist_id = w.id + JOIN catalogs AS c + ON c.id = wi.catalog_id + GROUP BY w.id, w.uuid, creator, + w.title, name, u.uuid, item_count, liked, w.description, + w.category, visibility, like_count, w.created_at, w.updated_at + ) + SELECT + jsonb_agg(jsonb_build_object( + 'id', t.id, + 'name', t.name + )) AS tags, fa.watchlist_id, fa.posters as posters, fa.watchlist_uuid, fa.creator, fa.creator_uuid, fa.item_count, fa.liked, fa.title, fa.description, fa.category, fa.visibility, fa.like_count, fa.created_at + FROM first_agg AS fa + LEFT JOIN watchlist_tag as wt ON wt.watchlist_id = fa.watchlist_id + LEFT JOIN tags as t ON t.id = wt.tag_id + GROUP BY + fa.watchlist_id, fa.watchlist_uuid, fa.posters, fa.creator, fa.creator_uuid, fa.item_count, fa.liked, fa.title, fa.description, fa.category, fa.visibility, fa.like_count, fa.created_at + ORDER BY + fa.created_at DESC + "; + + $pageCountQuery = " + SELECT COUNT(*) + FROM ( + SELECT w.id, w.user_id + FROM watchlists w + JOIN watchlist_save wv + ON w.id = wv.watchlist_id + WHERE wv.user_id = :user_id + ) AS w JOIN users AS u + ON w.user_id = u.id + JOIN (SELECT * FROM watchlist_items WHERE rank < 5) AS wi ON wi.watchlist_id = w.id + JOIN catalogs AS c ON c.id = wi.catalog_id + "; + + $selectStatement = $this->connection->prepare($selectQuery); + $selectStatement->bindParam(":user_id", $userId, PDO::PARAM_INT); + $selectStatement->bindParam(":limit", $pageSize, PDO::PARAM_INT); + $selectStatement->bindValue(":offset", ($page - 1) * $pageSize, PDO::PARAM_INT); + $selectStatement->execute(); + + $pageCountStatement = $this->connection->prepare($pageCountQuery); + $pageCountStatement->bindParam(":user_id", $userId, PDO::PARAM_INT); + $pageCountStatement->execute(); + + try { + $result = $selectStatement->fetchAll(PDO::FETCH_ASSOC); + $pageCount = $pageCountStatement->fetchColumn(); + + return [ + 'items' => $result, + 'page' => max(1, $page), + 'totalPage' => $pageSize > 0 ? ceil($pageCount / $pageSize) : 1 + ]; + } finally { + $selectStatement->closeCursor(); + $pageCountStatement->closeCursor(); + } + } + public function saveByWatchlistAndUser(string $watchlistId, string $userId) { $statement = $this->connection->prepare("INSERT INTO $this->table (watchlist_id, user_id) VALUES (:watchlist_id, :user_id)"); diff --git a/src/server/app/Repository/WatchlistTagRepository.php b/src/server/app/Repository/WatchlistTagRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..6e30dbc467ea439fbff7961932eaae4eb4bf98fc --- /dev/null +++ b/src/server/app/Repository/WatchlistTagRepository.php @@ -0,0 +1,13 @@ +<?php + +require_once __DIR__ . '/../App/Repository.php'; + +class WatchlistTagRepository extends Repository +{ + protected string $table = "watchlist_tag"; + + public function __construct(PDO $connection) + { + parent::__construct($connection); + } +} \ No newline at end of file diff --git a/src/server/app/Service/BookmarkService.php b/src/server/app/Service/BookmarkService.php new file mode 100644 index 0000000000000000000000000000000000000000..077f6d58632999d112ab53897974f600af96e1d0 --- /dev/null +++ b/src/server/app/Service/BookmarkService.php @@ -0,0 +1,26 @@ +<?php +require_once __DIR__ . '/../Config/Database.php'; +require_once __DIR__ . '/../Exception/ValidationException.php'; +require_once __DIR__ . '/../Utils/UUIDGenerator.php'; + +require_once __DIR__ . '/../Domain/WatchlistSave.php'; + +require_once __DIR__ . '/../Repository/WatchlistSaveRepository.php'; + +require_once __DIR__ . '/../Model/bookmark/BookmarkGetRequest.php'; + +class BookmarkService +{ + private WatchlistSaveRepository $watchlistSaveRepository; + + public function __construct(WatchlistSaveRepository $watchlistSaveRepository) + { + $this->watchlistSaveRepository = $watchlistSaveRepository; + } + + public function findByUser(BookmarkGetRequest $request) + { + $result = $this->watchlistSaveRepository->findByUser($request->userId, $request->page, $request->pageSize); + return $result; + } +} \ No newline at end of file diff --git a/src/server/app/Service/CatalogService.php b/src/server/app/Service/CatalogService.php index 32e82b83f3d9c05539d8e6a52b27660d439c6046..cc946570077ff8180b671de4511eea67f0983195 100644 --- a/src/server/app/Service/CatalogService.php +++ b/src/server/app/Service/CatalogService.php @@ -6,6 +6,7 @@ require_once __DIR__ . '/../Utils/FileUploader.php'; require_once __DIR__ . '/../Utils/UUIDGenerator.php'; require_once __DIR__ . '/../Model/CatalogCreateRequest.php'; +require_once __DIR__ . '/../Model/catalog/CatalogUpdateRequest.php'; require_once __DIR__ . '/../Model/CatalogSearchRequest.php'; require_once __DIR__ . '/../Model/CatalogCreateResponse.php'; @@ -48,7 +49,12 @@ class CatalogService public function deleteByUUID(string $uuid): void { - $this->catalogRepository->deleteBy('uuid', $uuid); + $catalog = $this->catalogRepository->findOne('uuid', $uuid); + if ($catalog) { + $this->catalogRepository->deleteBy('uuid', $uuid); + } else { + throw new ValidationException("Catalog not found."); + } } public function deleteById(int $id): void @@ -111,12 +117,18 @@ class CatalogService } } - public function update(string $uuid, CatalogCreateRequest $request) + public function update(CatalogUpdateRequest $request) { + $this->validateCatalogUpdateRequest($request); + try { Database::beginTransaction(); - $catalog = $this->catalogRepository->findOne('uuid', $uuid); + $catalog = $this->catalogRepository->findOne('uuid', $request->uuid); + + if (!$catalog) { + throw new ValidationException("Catalog not found."); + } $catalog->title = trim($request->title); $catalog->description = trim($request->description); @@ -147,6 +159,23 @@ class CatalogService } } + private function validateCatalogUpdateRequest(CatalogUpdateRequest $request) + { + if ($request->uuid == null || trim($request->uuid) == "") { + throw new ValidationException("UUID cannot be blank."); + } + + if ( + $request->title == null || trim($request->title) == "" + ) { + throw new ValidationException("Title cannot be blank."); + } + + if ($request->category == null || trim($request->category) == "") { + throw new ValidationException("Category cannot be blank."); + } + } + public function search(CatalogSearchRequest $catalogSearchRequest): CatalogSearchResponse { $this->validateCatalogSearchRequest($catalogSearchRequest); diff --git a/src/server/app/Service/TagService.php b/src/server/app/Service/TagService.php new file mode 100644 index 0000000000000000000000000000000000000000..7923586703f3b12646372e74bfde5db0017e2bde --- /dev/null +++ b/src/server/app/Service/TagService.php @@ -0,0 +1,19 @@ +<?php + +class TagService +{ + private TagRepository $tagRepository; + + public function __construct(TagRepository $tagRepository) + { + $this->tagRepository = $tagRepository; + } + + public function findAll() + { + $query = $this->tagRepository->query(); + $projection = ["id", "name"]; + $tags = $query->get($projection, 1, 100); + return $tags; + } +} \ No newline at end of file diff --git a/src/server/app/Service/UserService.php b/src/server/app/Service/UserService.php index 8da2aa77f29f4c236668e63e75f4d5dc93db25a8..f3f768014bead8cdcd6e7dcc393e3819b7939d40 100644 --- a/src/server/app/Service/UserService.php +++ b/src/server/app/Service/UserService.php @@ -135,9 +135,10 @@ class UserService } } - public function validateEditProfileRequest(UserEditRequest $request) + public function validateEditProfileRequest(User $currentuser, UserEditRequest $request) { - if (($request->oldPassword == null || trim($request->oldPassword) == "") + if ( + ($request->oldPassword == null || trim($request->oldPassword) == "") && ($request->newPassword == null || trim($request->newPassword) == "") && ($request->name == null || trim($request->name == "")) ) { @@ -168,7 +169,8 @@ class UserService throw new ValidationException("Old password cannot be blank."); } - if ((!($request->oldPassword == null || trim($request->oldPassword) == "") + if ( + (!($request->oldPassword == null || trim($request->oldPassword) == "") && !($request->newPassword == null || trim($request->newPassword) == "")) && ($request->oldPassword == $request->newPassword) ) { @@ -183,19 +185,18 @@ class UserService } } - - public function findByEmail(string $email): User { - return $this->userRepository->findByEmail($email); + return $this->userRepository->findOne('email', $email); } public function deleteByEmail(string $email) { - $this->userRepository->deleteByEmail($email); + $this->userRepository->deleteBy('email', $email); } + public function deleteBySession(string $email) { $this->userRepository->deleteBySession($email); } -} +} \ No newline at end of file diff --git a/src/server/app/Service/WatchlistService.php b/src/server/app/Service/WatchlistService.php index 3cc443af292c809d900ef8d3300a7cc9efaa535a..5ccd7f52c4a1a64095792ee98e91c86b347ca398 100644 --- a/src/server/app/Service/WatchlistService.php +++ b/src/server/app/Service/WatchlistService.php @@ -6,18 +6,21 @@ require_once __DIR__ . '/../Utils/UUIDGenerator.php'; require_once __DIR__ . '/../Domain/Watchlist.php'; require_once __DIR__ . '/../Domain/WatchlistItem.php'; require_once __DIR__ . '/../Domain/WatchlistLike.php'; +require_once __DIR__ . '/../Domain/WatchlistTag.php'; require_once __DIR__ . '/../Repository/WatchlistRepository.php'; require_once __DIR__ . '/../Repository/WatchlistItemRepository.php'; require_once __DIR__ . '/../Repository/WatchlistLikeRepository.php'; +require_once __DIR__ . '/../Repository/WatchlistTagRepository.php'; require_once __DIR__ . '/../Model/WatchlistsGetRequest.php'; require_once __DIR__ . '/../Model/WatchlistCreateRequest.php'; -require_once __DIR__ . '/../Model/watchlist/WatchlistGetSelfRequest.php'; +require_once __DIR__ . '/../Model/watchlist/WatchlistGetOneByUserRequest.php'; require_once __DIR__ . '/../Model/watchlist/WatchlistLikeRequest.php'; require_once __DIR__ . '/../Model/watchlist/WatchlistSaveRequest.php'; require_once __DIR__ . '/../Model/watchlist/WatchlistGetOneRequest.php'; require_once __DIR__ . '/../Model/watchlist/WatchlistEditRequest.php'; +require_once __DIR__ . '/../Model/watchlist/WatchlistDeleteRequest.php'; class WatchlistService { @@ -25,13 +28,15 @@ class WatchlistService private WatchlistItemRepository $watchlistItemRepository; private WatchlistLikeRepository $watchlistLikeRepository; private WatchlistSaveRepository $watchlistSaveRepository; + private WatchlistTagRepository $watchlistTagRepository; - public function __construct(WatchlistRepository $watchlistRepository, WatchlistItemRepository $watchlistItemRepository, WatchlistLikeRepository $watchlistLikeRepository, WatchlistSaveRepository $watchlistSaveRepository) + public function __construct(WatchlistRepository $watchlistRepository, WatchlistItemRepository $watchlistItemRepository, WatchlistLikeRepository $watchlistLikeRepository, WatchlistSaveRepository $watchlistSaveRepository, WatchlistTagRepository $watchlistTagRepository) { $this->watchlistRepository = $watchlistRepository; $this->watchlistItemRepository = $watchlistItemRepository; $this->watchlistLikeRepository = $watchlistLikeRepository; $this->watchlistSaveRepository = $watchlistSaveRepository; + $this->watchlistTagRepository = $watchlistTagRepository; } @@ -72,18 +77,27 @@ class WatchlistService // save the items $currRank = 1; foreach ($watchlistCreateRequest->items as $item) { - $watchlist_item = new WatchlistItem(); - $watchlist_item->uuid = UUIDGenerator::uuid4(); - $watchlist_item->rank = $currRank; - $watchlist_item->description = $item["description"]; - $watchlist_item->watchlistId = $watchlistNew->id; - $watchlist_item->catalogId = $item["id"]; + $watchlistItem = new WatchlistItem(); + $watchlistItem->uuid = UUIDGenerator::uuid4(); + $watchlistItem->rank = $currRank; + $watchlistItem->description = $item["description"]; + $watchlistItem->watchlistId = $watchlistNew->id; + $watchlistItem->catalogId = $item["id"]; - $this->watchlistItemRepository->save($watchlist_item); + $this->watchlistItemRepository->save($watchlistItem); $currRank++; } + // save the tags + foreach ($watchlistCreateRequest->tags as $tag) { + $watchlistTag = new WatchlistTag(); + $watchlistTag->tagId = $tag["id"]; + $watchlistTag->watchlistId = $watchlist->id; + + $this->watchlistTagRepository->save($watchlistTag); + } + Database::commitTransaction(); } catch (\Exception $exception) { Database::rollbackTransaction(); @@ -144,6 +158,18 @@ class WatchlistService $currRank++; } + // delete all tags with corresponding watchlistId + $this->watchlistTagRepository->deleteBy("watchlist_id", $watchlistNew->id); + + // save the tags + foreach ($watchlistEditRequest->tags as $tag) { + $watchlistTag = new WatchlistTag(); + $watchlistTag->tagId = $tag["id"]; + $watchlistTag->watchlistId = $watchlist->id; + + $this->watchlistTagRepository->save($watchlistTag); + } + Database::commitTransaction(); } catch (\Exception $exception) { Database::rollbackTransaction(); @@ -162,29 +188,32 @@ class WatchlistService if (!isset($watchlistsGetRequest->sortBy) || !in_array(strtoupper(trim($watchlistsGetRequest->sortBy)), ["DATE", "LOVE"])) { $watchlistsGetRequest->sortBy = "LOVE"; } + if (!isset($watchlistsGetRequest->tag) || !in_array(strtoupper(trim($watchlistsGetRequest->tag)), $watchlistsGetRequest->tagsInit)) { + $watchlistsGetRequest->tag = ""; + } if ($watchlistsGetRequest->sortBy == "LOVE") $watchlistsGetRequest->sortBy = "love_count"; if ($watchlistsGetRequest->sortBy == "DATE") $watchlistsGetRequest->sortBy = "w.created_at"; - $result = $this->watchlistRepository->findAllCustom($watchlistsGetRequest->userId, $watchlistsGetRequest->search, $watchlistsGetRequest->category, $watchlistsGetRequest->sortBy, $watchlistsGetRequest->order, $watchlistsGetRequest->page, 2); + $result = $this->watchlistRepository->findAllCustom($watchlistsGetRequest->userId, $watchlistsGetRequest->search, $watchlistsGetRequest->category, $watchlistsGetRequest->sortBy, $watchlistsGetRequest->order, $watchlistsGetRequest->tag, $watchlistsGetRequest->page, 2); return $result; } - public function findUserBookmarks(WatchlistsGetSelfRequest $request) + public function findByUser(WatchlistGetOneByUserRequest $request) { if (!isset($request->visibility) || !in_array(strtoupper(trim($request->visibility)), ["ALL", "PUBLIC", "PRIVATE"]) || strtoupper($request->visibility) == "ALL") { $request->visibility = ""; } - $result = $this->watchlistRepository->findUserBookmarks(1, strtoupper($request->visibility), 1, 10); + $result = $this->watchlistRepository->findByUser($request->userId, strtoupper($request->visibility), 1, 10); return $result; } public function findByUUID(WatchlistsGetOneRequest $request) { - $result = $this->watchlistRepository->findByUUID($request->uuid, null, $request->page, $request->pageSize); + $result = $this->watchlistRepository->findByUUID($request->uuid, $request->userId, $request->page, $request->pageSize); return $result; } @@ -245,27 +274,50 @@ class WatchlistService } } + public function deleteByUUID(WatchlistDeleteRequest $watchlistDeleteRequest) + { + $this->watchlistRepository->deleteBy("uuid", $watchlistDeleteRequest->watchlistUUID); + } + private function validateWatchlistCreateEditRequest(WatchlistCreateRequest|WatchlistEditRequest $watchlistCreateUpdateRequest) { if (!isset($watchlistCreateUpdateRequest->title) || trim($watchlistCreateUpdateRequest->title) == "") { - throw new ValidationException("Title is required"); + throw new ValidationException("Title is required."); + } + if (strlen($watchlistCreateUpdateRequest->title) > 40) { + throw new ValidationException("Title is too long. Maximum 40 chars."); } if (!isset($watchlistCreateUpdateRequest->visibility) || !in_array($watchlistCreateUpdateRequest->visibility, ["PUBLIC", "PRIVATE"])) { - throw new ValidationException("Invalid visibility"); + throw new ValidationException("Visibility is invalid."); } if (isset($watchlistCreateUpdateRequest->description) && strlen($watchlistCreateUpdateRequest->description) > 255) { - throw new ValidationException("Description is too long. Maximum 255 characters"); + throw new ValidationException("Description is too long. Maximum 255 characters."); } if (!isset($watchlistCreateUpdateRequest->items) || count($watchlistCreateUpdateRequest->items) == 0) { - throw new ValidationException("Watchlist must contain 1 item"); + throw new ValidationException("Watchlist must contain 1 item."); } if (count($watchlistCreateUpdateRequest->items) > 50) { - throw new ValidationException("Watchlist contains maximum 50 items"); + throw new ValidationException("Too many items. Maximum 50 items."); } foreach ($watchlistCreateUpdateRequest->items ?? [] as $item) { if (strlen($item["description"]) > 255) { - throw new ValidationException("Item description for" . $item["title"] . "is too long. Maximum 255 characters."); + throw new ValidationException("Description is too long for item ${item["title"]}. Maximum 255 chars."); + } + } + + $selectedTags = []; + foreach ($watchlistCreateUpdateRequest->tags ?? [] as $tag) { + $found = false; + foreach ($watchlistCreateUpdateRequest->initialTags ?? [] as $initTag) { + if ($tag["id"] == $initTag->id && !in_array($initTag->id, $selectedTags)) { + array_push($selectedTags, $initTag->id); + $found = true; + break; + } + } + if (!$found) { + throw new ValidationException("Tags is invalid."); } } } diff --git a/src/server/app/View/catalog/detail.php b/src/server/app/View/catalog/detail.php index 55f0514e9f9bcea5898156ee75884dc7bb8afbee..181a5cba49a09829da9b10b8b3f0fbbf5e680155 100644 --- a/src/server/app/View/catalog/detail.php +++ b/src/server/app/View/catalog/detail.php @@ -1,60 +1,42 @@ <?php -$data = $model['data'] +$catalog = $model['data']['item']; +$userRole = $model['data']['userRole']; ?> <main> <div class="catalog-detail-header"> <div class="catalog-detail-header-poster"></div> - <img class="poster" src="<?= '/assets/images/catalogs/posters/' . $data['poster'] ?>" - alt="<?= 'Poster of ' . $data['title'] ?>"> + <img class="poster" src="<?= '/assets/images/catalogs/posters/' . $catalog['poster'] ?>" + alt="<?= 'Poster of ' . $catalog['title'] ?>"> </div> <article class="catalog-detail-content"> <h2> - <?= $data['title'] ?> + <?= $catalog['title'] ?> </h2> <div class="tag"> - <?= $data['category'] ?> + <?= $catalog['category'] ?> </div> <p> - <?= $data['description'] ?? "No description" ?> + <?= (!isset($catalog['description']) || empty($catalog['description'])) ? "No description" : $catalog['description'] ?> </p> - <?php if (isset($data['trailer']) && $data['trailer'] !== null): ?> + <?php if (isset($catalog['trailer']) && $catalog['trailer'] !== null): ?> <h3>Trailer</h3> <video class="catalog-trailer" controls> - <source src="<?= '/assets/videos/catalogs/trailers/' . $data['trailer'] ?>" type="video/mp4"> + <source src="<?= '/assets/videos/catalogs/trailers/' . $catalog['trailer'] ?>" type="video/mp4"> </video> <?php endif; ?> </article> - <div class="button-container"> - <a href="/catalog/<?= $data['uuid'] ?>/edit" id="edit" aria-label="Edit <?= $data['title'] ?>" class="btn-icon"> - <?php require PUBLIC_PATH . 'assets/icons/edit.php' ?> - </a> - <button type="submit" aria-label="Delete <?= $data['title'] ?>" class="dialog-trigger btn-icon"> - <?php require PUBLIC_PATH . 'assets/icons/trash.php' ?> - </button> - </div> -</main> - - -<div class="dialog hidden"> - <div class="dialog__content"> - <h2> - Delete Catalog - </h2> - <p> - Are you sure you want to delete <span class="dialog-title"> - <?= $data['title'] ?> - </span>? - </p> - <div class="dialog__button-container"> - <button id="cancel"> - Cancel + <?php if ($userRole && $userRole === "ADMIN"): ?> + <div class="button-container"> + <a href="/catalog/<?= $catalog['uuid'] ?>/edit" id="edit" aria-label="Edit <?= $catalog['title'] ?>" + class="btn-icon"> + <?php require PUBLIC_PATH . 'assets/icons/edit.php' ?> + </a> + <button id="delete-trigger" type="submit" data-uuid="<?= $catalog['uuid'] ?>" + data-title="<?= $catalog['title'] ?>" aria-label="Delete <?= $catalog['title'] ?>" + class="catalog-delete-trigger dialog-trigger btn-icon"> + <?php require PUBLIC_PATH . 'assets/icons/trash.php' ?> </button> - <form action="/catalog/<?= $model['data']['uuid'] ?>/delete" method="POST"> - <button id="delete" class="btn-bold" type="submit"> - Delete - </button> - </form> </div> - </div> -</div> \ No newline at end of file + <?php endif; ?> +</main> \ No newline at end of file diff --git a/src/server/app/View/catalog/edit.php b/src/server/app/View/catalog/edit.php new file mode 100644 index 0000000000000000000000000000000000000000..5525bd2a388515ec66156535ae217a649f37738b --- /dev/null +++ b/src/server/app/View/catalog/edit.php @@ -0,0 +1,52 @@ +<?php function selectCategory($selected) +{ + $id = 'category'; + $placeholder = 'Select Category'; + $content = [ + "DRAMA", + "ANIME" + ]; + require __DIR__ . '/../components/select.php'; +} +?> + +<main> + <h2> + <?= $model['title'] ?> + </h2> + <form id="catalog-edit-form" enctype="multipart/form-data"> + <div class="input-group"> + <label class="input-required">Category</label> + <?php selectCategory($model['data']['category'] ?? 'ANIME'); ?> + </div> + <div class="input-group"> + <label for="titleField" class="input-required">Title</label> + <input type="text" id="titleField" name="title" placeholder="Title" + value="<?= $model['data']['title'] ?? "" ?>" maxlength="40" required> + </div> + <div class="input-group"> + <label for="descriptionField">Description</label> + <textarea placeholder="Enter description" name="description" id="descriptionField" maxlength="255"><?php if (isset($model['data'])) { + echo $model['data']['description']; + } ?></textarea> + </div> + <div class="input-group"> + <label for="posterField" class="input-required">Poster</label> + <img 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/*"> + </div> + <div class="input-group"> + <?php if (isset($model['data']['trailer']) && $model['data']['trailer'] !== null): ?> + <video 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 id="edit" class="btn-bold" type="submit"> + Edit + </button> + </form> +</main> \ No newline at end of file diff --git a/src/server/app/View/catalog/form.php b/src/server/app/View/catalog/form.php index 60f2a1794e6a3537aa1aabfd16c1461f17ddf7dc..20e49bd945603c37be56ec70c26c54dd29feef44 100644 --- a/src/server/app/View/catalog/form.php +++ b/src/server/app/View/catalog/form.php @@ -59,7 +59,7 @@ function alert($title, $message) </div> <div class="input-group"> <label for="trailerField">Trailer</label> - <input type="file" id="trailerField" name="trailer" accept="trailer/mp4"> + <input type="file" id="trailerField" name="trailer" accept="video/mp4"> </div> <button class="btn-bold" type="submit"> diff --git a/src/server/app/View/catalog/index.php b/src/server/app/View/catalog/index.php index 6bc19a3a652ab217e2a515e7b39970e97825de4a..a6df824829344d67024db762795792956b11baeb 100644 --- a/src/server/app/View/catalog/index.php +++ b/src/server/app/View/catalog/index.php @@ -11,7 +11,7 @@ function selectCategory($selected) require __DIR__ . '/../components/select.php'; } -function catalogCard(Catalog $catalog, bool $is_admin) +function catalogCard(Catalog $catalog, bool $isAdmin = false) { $title = $catalog->title; $poster = $catalog->poster; @@ -19,6 +19,7 @@ function catalogCard(Catalog $catalog, bool $is_admin) $description = $catalog->description; $uuid = $catalog->uuid; $id = $catalog->id; + $editable = $isAdmin; require __DIR__ . '/../components/card/catalogCard.php'; } @@ -38,12 +39,14 @@ function pagination(int $currentPage, int $totalPage) Apply </button> </form> - <a href="/catalog/create" class="btn btn-bold"> - <span class="icon-new"> - <?php require PUBLIC_PATH . 'assets/icons/plus.php' ?> - </span> - Add Catalog - </a> + <?php if ($model['data']['userRole'] && $model['data']['userRole'] === "ADMIN"): ?> + <a href="/catalog/create" class="btn btn-bold"> + <span class="icon-new"> + <?php require PUBLIC_PATH . 'assets/icons/plus.php' ?> + </span> + Add Catalog + </a> + <?php endif; ?> </section> <?php if (count($model['data']['catalogs']['items']) == 0): ?> <div class="no-item__container"> @@ -56,7 +59,7 @@ function pagination(int $currentPage, int $totalPage) <?php endif; ?> <section class="content"> <?php foreach ($model['data']['catalogs']['items'] ?? [] as $catalog): ?> - <?php catalogCard($catalog, $model['is_admin']); ?> + <?php catalogCard($catalog, $model['data']['userRole'] && $model['data']['userRole'] === "ADMIN"); ?> <?php endforeach; ?> <?php pagination($model['data']['catalogs']['page'], $model['data']['catalogs']['totalPage']); ?> </section> diff --git a/src/server/app/View/components/card/catalogCard.php b/src/server/app/View/components/card/catalogCard.php index 977ff25398d303b4d2b4e7ea32ba2879c693fd47..7fd441154cc70666d2fac417cfbf7fbf08649446 100644 --- a/src/server/app/View/components/card/catalogCard.php +++ b/src/server/app/View/components/card/catalogCard.php @@ -1,8 +1,15 @@ +<?php +if (!file_exists('assets/images/catalogs/posters/' . $poster)) { + $poster = 'no-poster.webp'; +} +?> + <div id="card-<?= $uuid ?>" class="card card-catalog"> <div class="card-content"> <a href="/catalog/<?= $uuid ?>"> - <img width="86.4" height="128" src="<?= "/assets/images/catalogs/posters/" . $poster ?>" alt=<?= $title ?> - class="poster" alt="<?= $title ?>" /> + <img width="86.4" height="128" onerror="this.src = '/assets/images/catalogs/posters/no-poster.webp'" + src="<?= "/assets/images/catalogs/posters/" . $poster ?>" alt=<?= $title ?> class="poster" + alt="<?= $title ?>" /> </a> <div class="card-body"> <a href="/catalog/<?= $uuid ?>"> @@ -18,38 +25,15 @@ </p> </div> </div> - <?php if (isset($is_admin) && $is_admin): ?> + <?php if (isset($editable) && $editable): ?> <div class="card-button-container"> <a aria-label="Edit <?= $title ?>" href="/catalog/<?= $uuid ?>/edit" id="edit-<?= $uuid ?>" class="btn-icon"> <?php require PUBLIC_PATH . 'assets/icons/edit.php' ?> </a> - <button aria-label="Delete <?= $title ?>" id="delete-<?= $uuid ?>" class="dialog-trigger btn-icon"> + <button aria-label="Delete <?= $title ?>" id="delete-trigger-<?= $uuid ?>" data-uuid="<?= $uuid ?>" + data-title="<?= $title ?>" class="catalog-delete-trigger dialog-trigger btn-icon"> <?php require PUBLIC_PATH . 'assets/icons/trash.php' ?> </button> </div> <?php endif; ?> -</div> - -<?php if (isset($is_admin) && $is_admin): ?> - <div id="dialog-<?= $uuid ?>" class="dialog hidden"> - <div class="dialog__content"> - <h2> - Delete Catalog - </h2> - <p> - Are you sure you want to delete - <?= $title ?>? - </p> - <div class="dialog__button-container"> - <button id="cancel"> - Cancel - </button> - <form action="/catalog/<?= $uuid ?>/delete" method="POST"> - <button id="delete" class="btn-bold" type="submit"> - Delete - </button> - </form> - </div> - </div> - </div> -<?php endif; ?> \ No newline at end of file +</div> \ 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 c666174f56efe33977fd89c454a9b9ae8df9c43e..2ea3518bbb9cd92b8e075e2b394070408474bd8b 100644 --- a/src/server/app/View/components/card/watchlistCard.php +++ b/src/server/app/View/components/card/watchlistCard.php @@ -24,13 +24,19 @@ if (!function_exists("likeAndSave")) { <div class="card-content"> <div class="list__poster"> <?php for ($i = 0; $i < 4; $i++): ?> - <?php if (!isset($posters[3 - $i])): ?> + <?php + 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"/> </div> <?php else: ?> + <?php + if (!file_exists('assets/images/catalogs/posters/' . $posters[3 - $i]['poster'])) { + $posters[3 - $i]['poster'] = 'no-poster.webp'; + } + ?> <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") ?>" @@ -59,6 +65,9 @@ if (!function_exists("likeAndSave")) { <span class="tag"> <?= $category ?> </span> + <?php for ($i = 0; $i < min(3, count($item["tags"])); $i++): ?> + <span class="tag"><?= $item["tags"][$i]["name"] ?></span> + <?php endfor; ?> <span class="subtitle">by <span class="author-name"> <?= $creator ?> </span></span> @@ -66,6 +75,7 @@ if (!function_exists("likeAndSave")) { <span class="subtitle"> <?= formatDate($createdAt); ?> </span> + </div> <p class="watchlist__description"> <?= $description ?> diff --git a/src/server/app/View/components/header.php b/src/server/app/View/components/header.php index a6f09da2b0a337652e6018341fad1c8dfccee3e9..7462e99aa2871161367df291d89f8bb259698110 100644 --- a/src/server/app/View/components/header.php +++ b/src/server/app/View/components/header.php @@ -4,7 +4,7 @@ <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <meta name="description" content="<?= $description ?? '#1 Drama and Anime Watch List Website' ?>"/> + <meta name="description" content="<?= $description ?? '#1 Drama and Anime Watch List Website' ?>" /> <title> <?= 'Drawl | ' . $model['title'] ?? '🌸' ?> @@ -23,7 +23,7 @@ <link rel="stylesheet" href="/css/components/modal.css"> <link rel='stylesheet' href='/css/components/alert.css'> --> - <?php foreach ($model['styles'] ?? [] as $style) : ?> + <?php foreach ($model['styles'] ?? [] as $style): ?> <link rel='stylesheet' href='<?= $style ?>'> <?php endforeach; ?> @@ -33,9 +33,8 @@ <script type="text/javascript" src="/js/components/select.js" defer></script> <script type="text/javascript" src="/js/components/modal.js" defer></script> <script type='text/javascript' src='/js/components/alert.js' defer></script> - <script type='text/javascript' src='/js/components/dialog.js' defer></script> - <?php foreach ($model['js'] ?? [] as $js) : ?> + <?php foreach ($model['js'] ?? [] as $js): ?> <script type='text/javascript' src='<?= $js ?>' defer></script> <?php endforeach; ?> </head> diff --git a/src/server/app/View/components/navbar.php b/src/server/app/View/components/navbar.php index 9bde832c4e66a4a02c3b2d56bef14dfc551850fc..5238aabb8e4c4c5d20926492c95285af1a0c12cb 100644 --- a/src/server/app/View/components/navbar.php +++ b/src/server/app/View/components/navbar.php @@ -5,18 +5,26 @@ <a href='/' class="brand__title">Drawl</a> </div> <button aria-label="Open Menu" id="navbar-toggle" class="navbar-toggle" aria-expanded="false" - aria-controls="navbar-menu"> + aria-controls="navbar-menu"> <?php require PUBLIC_PATH . 'assets/icons/menu.php' ?> </button> </div> <div id="navbar-menu" class="navbar-menu collapsed" aria-labelledby="navbar-toggle"> <a href="/" class="btn">Discover</a> <a href="/catalog" class="btn">Catalog</a> - <a href="/profile/watchlist" class="btn">My Watch List</a> <?php if ($user == null): ?> <a href="/signin" class="btn">Sign In</a> <?php else: ?> - <a href="/profile" class="btn"><?= $user->name ?></a> + <button id="profile-menu-toggle" class="profile-icon" aria-label="Open Profile Dropdown" + aria-expanded="false"> + <?php require PUBLIC_PATH . 'assets/icons/user.php' ?> + </button> + <div id="profile-menu" class="profile-menu collapsed" aria-labelledby="profile-menu-toggle"> + <a href="/profile" class="btn">Profile</a> + <a href="/profile/watchlist" class="btn">My Watchlist</a> + <a href="/profile/bookmark" class="btn">My Bookmark</a> + <button id="logout" class="btn">Logout</button> + </div> <?php endif; ?> </div> </div> diff --git a/src/server/app/View/components/toast.php b/src/server/app/View/components/toast.php new file mode 100644 index 0000000000000000000000000000000000000000..03e0577a8e9e2cc3574585020d4d4949f1e024dc --- /dev/null +++ b/src/server/app/View/components/toast.php @@ -0,0 +1,9 @@ +<div id="toast" class="toast hidden" data-type="error"> + <div> + <h3></h3> + <p></p> + </div> + <button id="close" class="btn-ghost"> + <?php require PUBLIC_PATH . "assets/icons/cancel.php" ?> + </button> +</div> \ No newline at end of file diff --git a/src/server/app/View/home/index.php b/src/server/app/View/home/index.php index a9b44a3abdee57bdeb8345e926020b0e43414237..cffdcb3a89d6f213740e2a96c2abdd9dcd9f6e65 100644 --- a/src/server/app/View/home/index.php +++ b/src/server/app/View/home/index.php @@ -24,6 +24,16 @@ function sortBy() require __DIR__ . '/../components/select.php'; } +function tags($tags) +{ + $id = 'tag'; + $placeholder = 'Select Tag'; + $content = $tags; + $selected = validateQueryParams($id, $content); + + require __DIR__ . '/../components/select.php'; +} + function vallidateOrder(): ?string { if (!isset($_GET["order"]) || ($_GET["order"] != "asc" && $_GET["order"] != "desc")) @@ -71,6 +81,7 @@ function pagination(int $currentPage, int $totalPage) value="<?= trim($_GET['search'] ?? '') ?? '' ?>"/> </div> <div class="filter"> + <?php tags($model["data"]["tags"]); ?> <?php selectCategory(); ?> <div class="filter__sort"> <?php sortBy(); ?> diff --git a/src/server/app/View/profile/bookmark.php b/src/server/app/View/profile/bookmark.php new file mode 100644 index 0000000000000000000000000000000000000000..4915cc04da0cbc1de03d50c883c85d9aa8855996 --- /dev/null +++ b/src/server/app/View/profile/bookmark.php @@ -0,0 +1,46 @@ +<?php + +function watchlistCard(array $item, string $userUUID, bool $saved = true, bool $loved = false, string $loading = "eager") +{ + $uuid = $item["watchlist_uuid"]; + $posters = $item["posters"]; + $visibility = $item["visibility"]; + $title = $item["title"]; + $category = $item["category"]; + $creator = $item["creator"]; + $createdAt = $item["created_at"]; + $description = $item["description"]; + $itemCount = $item["item_count"]; + $loveCount = $item["like_count"]; + $self = ($userUUID == $item["creator_uuid"]); + + + require __DIR__ . '/../components/card/watchlistCard.php'; +} + +function pagination(int $currentPage, int $totalPage) +{ + require __DIR__ . '/../components/pagination.php'; +} + +?> + +<main class="watchlist-self"> + <section class="search-filter"> + <h2>My Bookmark</h2> + </section> + <?php if (count($model['data']['bookmarks']['items']) == 0): ?> + <div class="no-item__container"> + <h1>Oops! 😣</h1> + <div> + <h2>There's No Bookmark Yet...</h2> + </div> + </div> + <?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 endfor; ?> + <?php pagination($model['data']['bookmarks']['page'], $model['data']['bookmarks']['totalPage']); ?> + </section> +</main> \ No newline at end of file diff --git a/src/server/app/View/watchlist/self.php b/src/server/app/View/profile/watchlist.php similarity index 98% rename from src/server/app/View/watchlist/self.php rename to src/server/app/View/profile/watchlist.php index 9c9e5d5333fd6c7e46139f721ba64e70f507556e..f4a4a066cfd56baeb9f8858db4eb2a0127146e1e 100644 --- a/src/server/app/View/watchlist/self.php +++ b/src/server/app/View/profile/watchlist.php @@ -8,7 +8,6 @@ function watchlistCard(array $item, string $userUUID, bool $saved = false, bool $title = $item["title"]; $category = $item["category"]; $creator = $item["creator"]; - $updatedAt = $item["updated_at"]; $createdAt = $item["created_at"]; $description = $item["description"]; $itemCount = $item["item_count"]; diff --git a/src/server/app/View/user/editProfile.php b/src/server/app/View/user/editProfile.php index aa393e8a8c8885dbc89d84ee69484f29a4ce7445..300694a9dd25b2cc26d93a670656aa5da0c34199 100644 --- a/src/server/app/View/user/editProfile.php +++ b/src/server/app/View/user/editProfile.php @@ -1,17 +1,37 @@ +<?php +function alert($title, $message, $type = 'error') +{ + require __DIR__ . '/../components/alert.php'; +} + +?> + + <main> <div class="edit-parameters"> <div class="my-profile-container"> - <h2><?= $model['data']['name'] ?> - Profile</h2> + <h2> + <?= $model['data']['name'] ?> - Profile + </h2> </div> - <form class="form-default" action="/editProfile" method="post"> + <?php if (isset($model['error'])): ?> + <?php alert('Failed to Update', $model['error']); ?> + <?php endif; ?> + <?php if (isset($model['success'])): ?> + <?php alert('Success', $model['success'], 'success'); ?> + <?php endif; ?> + <form id="profile-edit-form" class="form-default"> <div class="display-name"> <h3>Name</h3> + <p id="name"> + <?= $model['data']['name'] ?> + </p> </div> <p>Change name</p> <div class="input-container"> <div class="input-box"> - <input class="input" name="name"> - </input> + <input class="input" name="name" placeholder="Enter new name" + value="<?= $model['data']['name'] ?>" /> </div> </div> <div class="display-name"> @@ -31,8 +51,8 @@ </div> <div class="input-container"> <div class="input-box"> - <input class="input" type="password" name="oldPassword"> - </input> + <input class="input" type="password" name="oldPassword" + placeholder="Enter old password" /> </div> </div> </div> @@ -43,34 +63,20 @@ </div> <div class="input-container"> <div class="input-box"> - <input class="input" type="password" name="newPassword"> - - </input> + <input class="input" type="password" name="newPassword" + placeholder="Enter new password" /> </div> </div> </div> </div> </div> - <input class="btn-primary" name="update_button" type="submit" value="SAVE"> - - </input> - <input class="btn-primary" name="delete_button" type="submit" value="DELETE ACCOUNT"> - - </input> - + <button id="update-account" class="btn-primary save" name="update_button" type="submit"> + save + </button> + <button id="delete-account" class="btn-bold" type="button"> + Delete Account + </button> </form> - <?php if (isset($model['error'])) { ?> - <div class="alert-error"> - <p> - <?= $model['error'] ?> - </p> - </div> - <?php } ?> </div> - - - </div> - - </main> \ No newline at end of file diff --git a/src/server/app/View/user/signIn.php b/src/server/app/View/user/signIn.php index 8d0603f0887cb89e79f27a995fa2899f3e3a8b08..e633706dac1291e10d9613bcb16dd42a40cf8078 100644 --- a/src/server/app/View/user/signIn.php +++ b/src/server/app/View/user/signIn.php @@ -1,35 +1,41 @@ +<?php +function alert($title, $message) +{ + $type = 'error'; + require __DIR__ . '/../components/alert.php'; +} + +?> + + <div class="signin-container row"> - <img src="/assets/images/Suzume.webp" alt="Sign In Image" class="signin-poster"/> + <img src="/assets/images/Suzume.webp" alt="Sign In Image" class="signin-poster" /> <div class="right-side"> <div class="main-container"> <div class="welcome-text"> <h2 class="welcome-text__h2">Hello Again!</h2> <p class="welcome-text__h1">Welcome back! Please provide your details</p> </div> + <?php if (isset($model['error'])): ?> + <?php alert('Failed to Sign in', $model['error']); ?> + <?php endif; ?> <form class="inputs" action="/signin" method="post"> <div class="parameter"> <label for="email" class="input-required">Email</label> - <input type="email" name="email" id="email" class="input-default" - value="<?= $model['data']['email'] ?? "" ?>"> + <input type="email" name="email" id="email" class="input-default" placeholder="Enter email" + value="<?= $model['data']['email'] ?? "" ?>"> </div> <div class="parameter"> <label for="password" class="input-required">Password</label> - <input type="password" name="password" id="password" class="input-default"> + <input type="password" name="password" id="password" class="input-default" + placeholder="Enter password"> </div> <button class="btn-bold" type="submit"> Sign In </button> - <p>Don't have an account? </p> <a href="/signup" class="signup-link">Sign up</a> - + <p>Don't have an account? <a href="/signup" class="signup-link">Sign up</a></p> </form> - <?php if (isset($model['error'])) { ?> - <div class="alert-error"> - <p> - <?= $model['error'] ?> - </p> - </div> - <?php } ?> </div> </div> </div> \ No newline at end of file diff --git a/src/server/app/View/user/signUp.php b/src/server/app/View/user/signUp.php index b9cf731b49ba2efa26b81031064d1ca11d2edc5c..a9e507ea5650688a549ca6ccd404e140312a2e45 100644 --- a/src/server/app/View/user/signUp.php +++ b/src/server/app/View/user/signUp.php @@ -1,39 +1,46 @@ +<?php +function alert($title, $message) +{ + $type = 'error'; + require __DIR__ . '/../components/alert.php'; +} + +?> + <div class="signup-container row"> <img src="/assets/images/Tomorrow.webp" alt="Sign Up Image" class="signup-poster" /> <div class="right-side"> <div class="main-container"> <div class="welcome-text"> - <h2 class="welcome-text__h2">Let’s Get Started!</h2> + <h2 class="welcome-text__h2">Let's Get Started!</h2> <p class="welcome-text__h1">Glad to see you joining us! Please provide your details</p> </div> + <?php if (isset($model['error'])): ?> + <?php alert('Failed to Sign up', $model['error']); ?> + <?php endif; ?> + <form class="inputs" action="/signup" method="post"> <div class="parameter"> <label for="email" class="input-required">Email</label> - <input type="email" name="email" id="email" class="input-default" required> + <input type="email" name="email" id="email" class="input-default" required + placeholder="Enter email"> </div> <div class="parameter"> <label for="password" class="input-required">Password</label> - <input type="password" name="password" id="password" class="input-default" required /> + <input type="password" name="password" id="password" class="input-default" required + placeholder="Enter password" /> </div> <div class="parameter"> <label for="passwordConfirm" class="input-required">Confirm Password</label> - <input type="password" name="passwordConfirm" id="passwordConfirm" class="input-default" required /> + <input type="password" name="passwordConfirm" id="passwordConfirm" class="input-default" required + placeholder="Enter confirm password" /> </div> <button class="btn-bold" type="submit"> Sign Up </button> - + <p>Already have an account? <a href="/signin" class="signin-link">Sign in</a></p> </form> - - <?php if (isset($model['error'])) { ?> - <div class="alert-error"> - <p> - <?= $model['error'] ?> - </p> - </div> - <?php } ?> - </div> </div> </div> \ No newline at end of file diff --git a/src/server/app/View/watchlist/createUpdate.php b/src/server/app/View/watchlist/createUpdate.php index a60af9442b6d4cdee6ec918f82ce6033d1c525bf..12709f9a3b94217c5b4c4e4ca012054c9e3181a5 100644 --- a/src/server/app/View/watchlist/createUpdate.php +++ b/src/server/app/View/watchlist/createUpdate.php @@ -53,23 +53,46 @@ function watchlistItem($poster, $title, $id, $uuid, $category, $description) <input type="text" name="title" id="title" class="input-default" placeholder="Best Anime and Drama" value="<?= $model["data"]["title"] ?? '' ?>" required/> </div> + <div class="form-input-default"> <label for="description">Description</label> <textarea name="description" id="description" class="input-default" maxlength="255" placeholder="Enter your watchlist description"><?= $model["data"]["description"] ?? '' ?></textarea> </div> + <div class="form-input-default"> <label for="visibility" class="input-required">Visibility</label> - <?php visibility(isset($model["data"]) ? $model["data"]["visibility"] : null); ?> + <?php visibility(isset($model["data"]["visibility"]) ? $model["data"]["visibility"] : null); ?> + </div> + + <div class="form-input-default"> + <label for="tags">Tags</label> + <div class="tags"> + <?php foreach ($model["data"]["tags"] ?? [] as $tag): ?> + <?php $selected = false ?> + <?php foreach ($model["data"]["tagsSelected"] ?? [] as $ts): ?> + <?php if ($tag->id == $ts["id"]) $selected = true; ?> + <?php endforeach; ?> + <div class="input-tag"> + <label for="tag_<?= $tag->name ?>"><?= $tag->name ?></label> + <input type="checkbox" id="tag_<?= $tag->name ?>" name="<?= $tag->name ?>" + value="<?= $tag->id ?>" class="checkbox watchlist-tag" + <?= $selected ? "checked" : "" ?>/> + </div> + <?php endforeach; ?> + </div> </div> <h3 class="watchlist-items__title">Items</h3> <div class="watchlist-items"> - <?php foreach ((isset($model["data"]) ? $model["data"]["catalogs"]["items"] : []) as $item): ?> + <?php foreach ((isset($model["data"]["catalogs"]["items"]) ? $model["data"]["catalogs"]["items"] : []) as $item): ?> <div class="watchlist-item" draggable="true" data-id="<?= $item["catalog_uuid"] ?>"> <?php watchlistItem($item["poster"], $item["title"], $item["catalog_id"], $item["catalog_uuid"], $item["category"], $item["description"]); ?> </div> <?php endforeach; ?> + <?php if (!isset($model["data"]["catalogs"]["items"])): ?> + <p class="items-placeholder">No items selected.</p> + <?php endif; ?> </div> <input id="input-submit" type="submit" class="hidden"/> diff --git a/src/server/app/View/watchlist/detail.php b/src/server/app/View/watchlist/detail.php index 21d2d12dc75c2f4309a23ad86cc550feadacc2e5..60a8a54a233701a6eb857ea91d2c0931a84fcc15 100644 --- a/src/server/app/View/watchlist/detail.php +++ b/src/server/app/View/watchlist/detail.php @@ -1,12 +1,19 @@ <?php +if (!function_exists("formatDate")) { + function formatDate($createdAt) + { + require __DIR__ . '/../../../config/dateFormat.php'; + } +} + function catalogCard($catalog) { - $is_admin = false; $title = $catalog['title']; $poster = $catalog['poster']; $category = $catalog['category']; $description = $catalog['description']; + $uuid = $catalog['catalog_uuid']; require __DIR__ . '/../components/card/catalogCard.php'; } @@ -14,58 +21,94 @@ function pagination($currentPage, $totalPage) { require __DIR__ . '/../components/pagination.php'; } + +if (!function_exists("likeAndSave")) { + + function likeAndSave($class, $icon) + { + $triggerClasses = "btn-ghost $class"; + $triggerText = ""; + $triggerIcon = $icon; + $title = "Sign In Required"; + $content = 'signInRequired'; + require __DIR__ . '/../components/modal.php'; + } +} + ?> <main> <article class="header"> <div class="detail"> <h2> - <?= $model['data']['title'] ?> + <?= $model['data']['item']['title'] ?> </h2> <div class="container-subtitle"> <div class="tag"> - <?= $model['data']['category'] ?> + <?= $model['data']['item']['category'] ?> </div> <p class="subtitle"> - <?= $model['data']['creator'] ?> | - <?= $model['data']['updated_at'] ?> + <?= $model['data']['item']['creator'] ?> | + <?= formatDate($model['data']['item']['created_at']) ?> </p> </div> + <div class="watchlist__wrapper-type-author"> + <?php foreach ($model["data"]["item"]["tags"] as $tag): ?> + <span class="tag"><?= $tag["name"] ?></span> + <?php endforeach; ?> + </div> <p> - <?= $model['data']['description'] ?> + <?= $model['data']['item']['description'] ?> </p> </div> <div class="container-button"> <div class="container-btn-love"> - <button class="btn-ghost"> - <?php - $type = (isset($model['data']['like_status']) && $model['data']['like_status']) ? "filled" : "unfilled"; - require PUBLIC_PATH . 'assets/icons/love.php' ?> - </button> - <span> - <?= $model['data']['like_count'] ?> + <?php if ($model['data']['userUUID'] == ""): ?> + <?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"] ?>"> + <?php + $type = (isset($model['data']["item"]['liked']) && $model['data']["item"]['liked']) ? "filled" : "unfilled"; + require PUBLIC_PATH . 'assets/icons/love.php' ?> + </button> + <?php endif; ?> + <span data-id="<?= $model["data"]["item"]["watchlist_uuid"] ?>"> + <?= $model['data']['item']['like_count'] ?> </span> </div> - <button class="btn-ghost"> - <?php - $type = (isset($model['data']['save_status']) && $model['data']['save_status']) ? "filled" : "unfilled"; - require PUBLIC_PATH . 'assets/icons/bookmark.php' ?> - </button> + <div class="container-btn-love"> + <?php if (!isset($model['data']['userUUID']) || $model['data']['userUUID'] != $model['data']['item']['creator_uuid']): ?> + <?php if ($model['data']['userUUID'] == ""): ?> + <?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"] ?>" + > + <?php + $type = (isset($model['data']['item']['saved']) && $model['data']['item']['saved']) ? "filled" : "unfilled"; + require PUBLIC_PATH . 'assets/icons/bookmark.php' ?> + </button> + <?php endif; ?> + <?php endif; ?> + </div> </div> </article> <article id="catalogs" class="content"> - <?php foreach ($model['data']['catalogs']['items'] ?? [] as $catalog): ?> + <?php foreach ($model['data']['item']['catalogs']['items'] ?? [] as $catalog): ?> <?php catalogCard($catalog); ?> <?php endforeach; ?> - <?php pagination($model['data']['catalogs']['page'], $model['data']['catalogs']['totalPage']); ?> + <?php pagination($model['data']['item']['catalogs']['page'], $model['data']['item']['catalogs']['totalPage']); ?> </article> - <?php if (isset($model['editable']) && $model['editable']): ?> + <?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']['watchlist_uuid'] . "/edit" ?>" id="edit" - aria-label="Edit <?= $model['data']['title'] ?>" class="btn-icon"> + <a href="<?= "/watchlist/" . $model['data']['item']['watchlist_uuid'] . "/edit" ?>" id="edit" + 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']['title'] ?>" class="dialog-trigger btn-icon"> + <button type="submit" aria-label="Delete <?= $model['data']['item']['title'] ?>" + 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/config/database.php b/src/server/config/database.php index bd9a825eaf7c687febf4e0f88829f27091e51963..b0482b033cdd7ba5fadcc817ba5ac812be11a1d8 100644 --- a/src/server/config/database.php +++ b/src/server/config/database.php @@ -13,7 +13,6 @@ function getDatabaseConfig(): array 'username' => getenv('DB_USER'), 'password' => getenv('DB_PASSWORD'), ], - "prod" => [], ], ]; } diff --git a/src/server/routes/view.php b/src/server/routes/view.php index 5ae2e254278e520b5b1d5a6975cc1600cbabda47..ea94b3e86be137c7459e0794b9a8f185ec698754 100644 --- a/src/server/routes/view.php +++ b/src/server/routes/view.php @@ -6,6 +6,7 @@ require_once __DIR__ . "/../app/Controller/UserController.php"; require_once __DIR__ . "/../app/Controller/CatalogController.php"; require_once __DIR__ . '/../app/Controller/WatchlistController.php'; require_once __DIR__ . '/../app/Controller/ErrorPageController.php'; +require_once __DIR__ . '/../app/Controller/BookmarkController.php'; require_once __DIR__ . '/../app/Middleware/UserAuthMiddleware.php'; require_once __DIR__ . '/../app/Middleware/AdminAuthMiddleware.php'; @@ -23,11 +24,12 @@ Router::add('GET', '/signup', UserController::class, 'signUp', []); Router::add('POST', '/signup', UserController::class, 'postSignUp', []); Router::add('GET', '/signin', UserController::class, 'signIn', []); Router::add('POST', '/signin', UserController::class, 'postSignIn', []); -Router::add('GET', '/editProfile', UserController::class, 'showEditProfile', [UserAuthMiddleware::class]); -Router::add('POST', '/editProfile', UserController::class, 'postEditProfile', [UserAuthMiddleware::class]); +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]); // Catalog controllers -Router::add('GET', '/catalog', CatalogController::class, 'index', [AdminAuthMiddleware::class]); +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]); @@ -35,6 +37,8 @@ Router::add('POST', '/catalog/([A-Za-z0-9]*)/edit', CatalogController::class, 'p 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', '/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]); // Watchlist controllers Router::add("GET", "/watchlist/create", WatchlistController::class, 'create', [UserAuthMiddleware::class]); @@ -43,11 +47,14 @@ Router::add("GET", "/watchlist/([A-Za-z0-9]*)", WatchlistController::class, 'det Router::add("POST", "/api/watchlist", WatchlistController::class, "postCreate", [UserAuthApiMiddleware::class]); Router::add("PUT", "/api/watchlist", WatchlistController::class, "putEdit", [UserAuthApiMiddleware::class]); +Router::add("DELETE", "/api/watchlist", WatchlistController::class, "delete", [UserAuthApiMiddleware::class]); Router::add("GET", "/api/watchlist/item", WatchlistController::class, 'item', [UserAuthApiMiddleware::class]); Router::add("POST", "/api/watchlist/like", WatchlistController::class, "like", [UserAuthApiMiddleware::class]); Router::add("POST", "/api/watchlist/save", WatchlistController::class, "bookmark", [UserAuthApiMiddleware::class]); +Router::add('GET', '/profile', UserController::class, 'showEditProfile', [UserAuthMiddleware::class]); +Router::add('GET', '/profile/bookmark', BookmarkController::class, 'self', [UserAuthMiddleware::class]); Router::add('GET', '/profile/watchlist', WatchlistController::class, 'self', [UserAuthMiddleware::class]); // Error page @@ -55,4 +62,4 @@ Router::add('GET', '/404', ErrorPageController::class, 'fourohfour', []); Router::add('GET', '/500', ErrorPageController::class, 'fivehundred', []); // Execute -Router::run(); +Router::run(); \ No newline at end of file