diff --git a/.gitignore b/.gitignore index 7e10641fc4e7aca641c85458f9f2b1a1429d4473..233d4e405c5562dc62bc34dce272306723e5f6a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ db/ .env -.DS_Store \ No newline at end of file +.DS_Store +src/server/html/ \ No newline at end of file diff --git a/src/migration/db.sql b/src/migration/db.sql index edc7c9e4040766e618638466f3de2512db4e1a97..087d5b140f6071ff03140523d73fcbdf6f7da7c7 100644 --- a/src/migration/db.sql +++ b/src/migration/db.sql @@ -19,9 +19,9 @@ CREATE TABLE IF NOT EXISTS users ( ); CREATE TABLE IF NOT EXISTS sessions ( - id SERIAL PRIMARY KEY, + id VARCHAR(16) PRIMARY KEY, user_id INT NOT NULL, - expired TIMESTAMPTZ DEFAULT NOW() + INTERVAL '7 days', + expired TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, @@ -172,4 +172,10 @@ CREATE TABLE IF NOT EXISTS watchlist_tag ( PRIMARY KEY (watchlist_id, tag_id), FOREIGN KEY (watchlist_id) REFERENCES watchlists(id), FOREIGN KEY (tag_id) REFERENCES tags(id) -); \ No newline at end of file +); + +ALTER TABLE users ADD CONSTRAINT users_uuid_key UNIQUE (uuid); +ALTER TABLE catalogs ADD CONSTRAINT catalogs_uuid_key UNIQUE (uuid); +ALTER TABLE watchlists ADD CONSTRAINT watchlists_uuid_key UNIQUE (uuid); +ALTER TABLE comments ADD CONSTRAINT comments_uuid_key UNIQUE (uuid); +ALTER TABLE tags ADD CONSTRAINT tags_name_key UNIQUE (name); \ No newline at end of file diff --git a/src/public/assets/fonts/karla/karla-latin-400-normal.ttf b/src/public/assets/fonts/karla/karla-latin-400-normal.woff similarity index 100% rename from src/public/assets/fonts/karla/karla-latin-400-normal.ttf rename to src/public/assets/fonts/karla/karla-latin-400-normal.woff diff --git a/src/public/assets/icons/asc.php b/src/public/assets/icons/asc.php index 2e1bd2753fb2d6029807214e3d5bb7635172cdb0..ed0a098546505309928376c69d508bec676bf9e7 100644 --- a/src/public/assets/icons/asc.php +++ b/src/public/assets/icons/asc.php @@ -1,4 +1,4 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-narrow-wide"> +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-asc"> <path d="m3 8 4-4 4 4" /> <path d="M7 4v16" /> <path d="M11 12h4" /> diff --git a/src/public/assets/icons/cancel.php b/src/public/assets/icons/cancel.php new file mode 100644 index 0000000000000000000000000000000000000000..706d750d093c02f125bddf7451798674dae73f74 --- /dev/null +++ b/src/public/assets/icons/cancel.php @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none"> + <path d="M1 13L7.00001 7.00002M7.00001 7.00002L13 1M7.00001 7.00002L1 1M7.00001 7.00002L13 13" stroke="black" + stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> +</svg> \ No newline at end of file diff --git a/src/public/assets/icons/clapperboard.php b/src/public/assets/icons/clapperboard.php new file mode 100644 index 0000000000000000000000000000000000000000..f225387f69df44cac4dc80c984eebeea43ff80f0 --- /dev/null +++ b/src/public/assets/icons/clapperboard.php @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-clapperboard"> + <path d="M20.2 6 3 11l-.9-2.4c-.3-1.1.3-2.2 1.3-2.5l13.5-4c1.1-.3 2.2.3 2.5 1.3Z" /> + <path d="m6.2 5.3 3.1 3.9" /> + <path d="m12.4 3.4 3.1 4" /> + <path d="M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" /> +</svg> \ No newline at end of file diff --git a/src/public/assets/icons/error.php b/src/public/assets/icons/error.php new file mode 100644 index 0000000000000000000000000000000000000000..87c5c9a9422cb6e0512865e4361da84a0cef1748 --- /dev/null +++ b/src/public/assets/icons/error.php @@ -0,0 +1,9 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none"> + <path + d="M9 17C13.4182 17 17 13.4182 17 9C17 4.58172 13.4182 1 9 1C4.58172 1 1 4.58172 1 9C1 13.4182 4.58172 17 9 17Z" + stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> + <path d="M11.4006 12.2003L6.60059 5.80029" stroke="black" stroke-width="1.5" stroke-linecap="round" + stroke-linejoin="round" /> + <path d="M6.60059 12.2003L11.4006 5.80029" stroke="black" stroke-width="1.5" stroke-linecap="round" + stroke-linejoin="round" /> +</svg> \ No newline at end of file diff --git a/src/public/assets/icons/info.php b/src/public/assets/icons/info.php new file mode 100644 index 0000000000000000000000000000000000000000..217ef2466fbf62301feabd4d6cb86bc642cf3aee --- /dev/null +++ b/src/public/assets/icons/info.php @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="2" height="3" viewBox="0 0 2 3" fill="none"> + <path d="M1 1.30598L1.006 1.29932" stroke="black" stroke-width="1.5" stroke-linecap="round" + stroke-linejoin="round" /> +</svg> \ No newline at end of file diff --git a/src/public/assets/icons/success.php b/src/public/assets/icons/success.php new file mode 100644 index 0000000000000000000000000000000000000000..bd6348451040b9a68d5e49576020b114c044485e --- /dev/null +++ b/src/public/assets/icons/success.php @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none"> + <path + d="M7 13C10.3137 13 13 10.3137 13 7C13 3.68629 10.3137 1 7 1C3.68629 1 1 3.68629 1 7C1 10.3137 3.68629 13 7 13Z" + stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> +</svg> \ No newline at end of file diff --git a/src/public/assets/icons/x.php b/src/public/assets/icons/x.php new file mode 100644 index 0000000000000000000000000000000000000000..b5aceff64dd9692669d48b3680d022fc93af4dd1 --- /dev/null +++ b/src/public/assets/icons/x.php @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-x"> + <path d="M18 6 6 18" /> + <path d="m6 6 12 12" /> +</svg> \ No newline at end of file diff --git a/src/public/assets/images/catalogs/posters/6517b8128a6c3_Tomorrow.webp b/src/public/assets/images/catalogs/posters/6517b8128a6c3_Tomorrow.webp new file mode 100644 index 0000000000000000000000000000000000000000..a3ab0acfb409986a601c1d18371d8e11f6371999 Binary files /dev/null and b/src/public/assets/images/catalogs/posters/6517b8128a6c3_Tomorrow.webp differ diff --git a/src/public/assets/images/catalogs/posters/6517b8f0bd688_Tomorrow.webp b/src/public/assets/images/catalogs/posters/6517b8f0bd688_Tomorrow.webp new file mode 100644 index 0000000000000000000000000000000000000000..a3ab0acfb409986a601c1d18371d8e11f6371999 Binary files /dev/null and b/src/public/assets/images/catalogs/posters/6517b8f0bd688_Tomorrow.webp differ diff --git a/src/public/assets/images/catalogs/posters/6517b94da6c07_Elaina.webp b/src/public/assets/images/catalogs/posters/6517b94da6c07_Elaina.webp new file mode 100644 index 0000000000000000000000000000000000000000..4a22e66858e329dee7e0af36137f1f6382976506 Binary files /dev/null and b/src/public/assets/images/catalogs/posters/6517b94da6c07_Elaina.webp differ diff --git a/src/public/assets/images/catalogs/posters/6517f04670806_Tes.jpeg b/src/public/assets/images/catalogs/posters/6517f04670806_Tes.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..fb99d24620d33127eeffbe15b25cd5d2adede8c2 Binary files /dev/null and b/src/public/assets/images/catalogs/posters/6517f04670806_Tes.jpeg differ diff --git a/src/public/assets/images/catalogs/posters/6517f0c841c4a_Halo.jpeg b/src/public/assets/images/catalogs/posters/6517f0c841c4a_Halo.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..fb99d24620d33127eeffbe15b25cd5d2adede8c2 Binary files /dev/null and b/src/public/assets/images/catalogs/posters/6517f0c841c4a_Halo.jpeg differ diff --git a/src/public/assets/images/catalogs/posters/6518150def1dc_Snowdrop.webp b/src/public/assets/images/catalogs/posters/6518150def1dc_Snowdrop.webp new file mode 100644 index 0000000000000000000000000000000000000000..fb99d24620d33127eeffbe15b25cd5d2adede8c2 Binary files /dev/null and b/src/public/assets/images/catalogs/posters/6518150def1dc_Snowdrop.webp differ diff --git a/src/public/assets/images/catalogs/posters/no-poster.webp b/src/public/assets/images/catalogs/posters/no-poster.webp new file mode 100644 index 0000000000000000000000000000000000000000..f5a0f34071dd9e57d1c1ced894189082f60c6140 Binary files /dev/null and b/src/public/assets/images/catalogs/posters/no-poster.webp differ diff --git a/src/server/test/Config/DatabaseTest.php b/src/public/assets/videos/catalogs/trailers/6517bd1c9f2a7_Frieren similarity index 100% rename from src/server/test/Config/DatabaseTest.php rename to src/public/assets/videos/catalogs/trailers/6517bd1c9f2a7_Frieren diff --git a/src/public/assets/videos/catalogs/trailer/the-journey-of-elaina.mp4 b/src/public/assets/videos/catalogs/trailers/6517bd1c9f2a7_FrierenBeyond's the Journey Ends.mp4 similarity index 100% rename from src/public/assets/videos/catalogs/trailer/the-journey-of-elaina.mp4 rename to src/public/assets/videos/catalogs/trailers/6517bd1c9f2a7_FrierenBeyond's the Journey Ends.mp4 diff --git a/src/public/assets/videos/catalogs/trailers/the-journey-of-elaina.mp4 b/src/public/assets/videos/catalogs/trailers/the-journey-of-elaina.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..99c1c2430523e47ea2c8a065c65278533fc26d0a Binary files /dev/null and b/src/public/assets/videos/catalogs/trailers/the-journey-of-elaina.mp4 differ diff --git a/src/public/css/form-catalog.css b/src/public/css/catalog-form.css similarity index 100% rename from src/public/css/form-catalog.css rename to src/public/css/catalog-form.css diff --git a/src/public/css/catalog.css b/src/public/css/catalog.css index 2b21391d509358911cc0f3c0a6c6c0f593596e69..969bbb9e01de4f91f15e16d52885c201abcf0e33 100644 --- a/src/public/css/catalog.css +++ b/src/public/css/catalog.css @@ -1,13 +1,3 @@ -.container__catalog { - padding: 0 1rem; - max-width: 80rem; - width: 100%; - margin: 3rem auto; - position: relative; - z-index: 10; - gap: 2rem; -} - form { width: 100%; display: flex; diff --git a/src/public/css/components/alert.css b/src/public/css/components/alert.css new file mode 100644 index 0000000000000000000000000000000000000000..b754f83ebe39def252b892bb9b0be7183f5bc0f3 --- /dev/null +++ b/src/public/css/components/alert.css @@ -0,0 +1,96 @@ +.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 index 14516ffbeeda8c2a2a60ae53cf9b3eb3210b86ff..0ccab5359b241bd31e3f05b9d1c052142df6b129 100644 --- a/src/public/css/components/button.css +++ b/src/public/css/components/button.css @@ -92,4 +92,12 @@ button:hover { .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 index 8d7dfe2b96ca4c984e43c0c7d056578259e5bb76..68cb557e0c200468e3140153c986e64bb094e555 100644 --- a/src/public/css/components/card.css +++ b/src/public/css/components/card.css @@ -11,6 +11,7 @@ } .card-content { + width: 100%; display: flex; align-items: flex-start; gap: 24px; @@ -29,7 +30,7 @@ overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; - -webkit-line-clamp: 6; + -webkit-line-clamp: 3; } .card-button-container { diff --git a/src/public/css/components/input.css b/src/public/css/components/input.css index 1227c284b4588b48c809c36b582a4c789aa7a524..cf697682b68fc3d5a7d111d375f67d0d1c266c3b 100644 --- a/src/public/css/components/input.css +++ b/src/public/css/components/input.css @@ -27,10 +27,9 @@ input { label { color: var(--slate-900, #0F172A); - font-size: 14px; + font-size: 0.875rem; font-style: normal; font-weight: 400; - line-height: 20px; } textarea { @@ -43,4 +42,5 @@ textarea { 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 new file mode 100644 index 0000000000000000000000000000000000000000..a575184389be6dea7bd4ea224f2f0b4907f3b04a --- /dev/null +++ b/src/public/css/components/modal.css @@ -0,0 +1,53 @@ +.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: 80vh; + 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); +} + +.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/modal/watchlistAddItem.css b/src/public/css/components/modal/watchlistAddItem.css new file mode 100644 index 0000000000000000000000000000000000000000..db8cbd4bfc2c155948d10610e6e8a5f550a5a548 --- /dev/null +++ b/src/public/css/components/modal/watchlistAddItem.css @@ -0,0 +1,14 @@ +.form-search { + display: flex; +} + +.search { + display: flex; + align-items: center; + width: 100%; + gap: 0.4rem; +} + +.search__input { + flex-grow: 1; +} diff --git a/src/public/css/components/select.css b/src/public/css/components/select.css index ea71688c143509410d80f0acdf5d34c032e0063d..f31d8847eac3024e80a2fcd9652ee2c929b48c5e 100644 --- a/src/public/css/components/select.css +++ b/src/public/css/components/select.css @@ -24,7 +24,7 @@ border-radius: 0.4rem; min-width: 200px; width: 100%; - z-index: 10; + z-index: 50; } .c-select-options .c-select-option { diff --git a/src/public/css/components/tag.css b/src/public/css/components/tag.css deleted file mode 100644 index bd3413d74d397760eeea3d98555c095d5eeadfbc..0000000000000000000000000000000000000000 --- a/src/public/css/components/tag.css +++ /dev/null @@ -1,15 +0,0 @@ -.tag { - display: flex; - padding: 0px 10px; - justify-content: center; - align-items: center; - gap: 10px; - border-radius: 8px; - background: var(--accent-300, #EDDEF6); - width: fit-content; - - font-size: 0.8rem; - font-style: normal; - font-weight: 400; - text-transform: lowercase; -} \ No newline at end of file diff --git a/src/public/css/components/textarea.css b/src/public/css/components/textarea.css new file mode 100644 index 0000000000000000000000000000000000000000..3381a355f56400673c67128142287afef647430b --- /dev/null +++ b/src/public/css/components/textarea.css @@ -0,0 +1,3 @@ +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 7a72de5ad9255d303ce81af699ab249c512c2882..2b5eebac4ef5903c0d78f78a202339b7ac67ed7d 100644 --- a/src/public/css/global.css +++ b/src/public/css/global.css @@ -25,9 +25,18 @@ --slate-700: #334155; --slate-800: #1e293b; --slate-900: #0f172a; + + --error-background: #FEE2E2; + --error-foreground: #B91C1C; + + --info-background: #dbeafe; + --info-foreground: #1d4ed8; + + --success-background: #dcfce7; + --success-foreground:#15803d; } -@font-face { +/* @font-face { font-family: 'Karla'; src: url(/assets/fonts/karla/karla-latin-200-normal.ttf); font-weight: 200; @@ -38,32 +47,32 @@ src: url(/assets/fonts/karla/karla-latin-200-italic.ttf); font-weight: 200; font-style: italic; -} +} */ @font-face { font-family: 'Karla'; - src: url(/assets/fonts/karla/karla-latin-400-normal.ttf); + src: url(/assets/fonts/karla/karla-latin-400-normal.woff); font-weight: 400; font-style: normal; } -@font-face { +/* @font-face { font-family: 'Karla'; src: url(/assets/fonts/karla/karla-latin-400-italic.ttf); font-weight: 400; font-style: italic; -} -@font-face { +} */ +/* @font-face { font-family: 'Karla'; src: url(/assets/fonts/karla/karla-latin-600-normal.ttf); font-weight: 600; font-style: normal; -} -@font-face { +} */ +/* @font-face { font-family: 'Karla'; src: url(/assets/fonts/karla/karla-latin-600-italic.ttf); font-weight: 600; font-style: italic; -} -@font-face { +} */ +/* @font-face { font-family: 'Karla'; src: url(/assets/fonts/karla/karla-latin-800-normal.ttf); font-weight: 800; @@ -74,7 +83,7 @@ src: url(/assets/fonts/karla/karla-latin-800-italic.ttf); font-weight: 800; font-style: italic; -} +} */ * { inset: unset; @@ -94,10 +103,10 @@ body { h2 { color: var(--accent-800); - font-size: 24px; + font-size: 1.5rem; font-style: normal; font-weight: 600; - line-height: 24px; + line-height: 1.5rem; } h3 { @@ -112,7 +121,7 @@ h4 { p { color: var(--slate-700, #334155); - font-size: 14px; + font-size: 0.875rem; font-style: normal; font-weight: 400; line-height: normal; @@ -120,7 +129,7 @@ p { p.subtitle { color: var(--slate-400, #94A3B8); - font-size: 10px; + font-size: 0.625rem; font-style: normal; font-weight: 400; line-height: normal; @@ -143,7 +152,8 @@ a { justify-content: center; } -.container__default { +main, +.container { display: flex; flex-direction: column; padding: 0 1rem; @@ -195,14 +205,14 @@ a { } .hidden { - display: none; + display: none !important; } .content { display: flex; flex-direction: column; align-items: flex-start; - gap: 24px; + gap: 1.5rem; align-self: stretch; } @@ -213,21 +223,46 @@ a { } .poster { - height: 10rem; - width: 7.4rem; + transition: top ease 1s; + position: relative; + height: 8rem; + width: 5.4rem; object-fit: cover; - border-radius: 1rem; + border-radius: 0.6rem; box-shadow: rgba(104, 104, 104, 0.45) 5px 2.4px 5px; } +.tag { + display: flex; + padding: 0px 10px; + justify-content: center; + align-items: center; + gap: 10px; + border-radius: 8px; + background: var(--accent-300, #EDDEF6); + width: fit-content; + + font-size: 0.8rem; + font-style: normal; + font-weight: 400; + text-transform: lowercase; +} + +@media screen and (min-width: 500px) { + .poster { + height: 10rem; + width: 7.4rem; + } +} + .avatar { display: flex; - width: 40px; - height: 40px; + width: 2.5rem; + height: 2.5rem; justify-content: center; align-items: center; flex-shrink: 0; - border-radius: 40px; + border-radius: 2.5rem; object-fit: cover; background: url(<path-to-image>), lightgray 50% / cover no-repeat; } diff --git a/src/public/css/home.css b/src/public/css/home.css index d091c05cc5cb0c807cf985fa4a4c8194dbcb52d8..347d7cfe7e9c2e80b4e5fca6e4483b35f23d9490 100644 --- a/src/public/css/home.css +++ b/src/public/css/home.css @@ -37,6 +37,7 @@ justify-content: left; align-items: center; gap: 1rem; + z-index: 50; } .filter__sort { @@ -47,11 +48,7 @@ width: 100%; } -@media screen and (min-width: 1024px) { - .filter { - flex-direction: row; - } -} + .btn-sort { padding: 0.5rem; @@ -67,65 +64,71 @@ max-width: none; } -@media screen and (min-width: 1024px) { - .btn--apply { - max-width: fit-content; - } -} + .icon-new { font-size: 12px; } -.catalog-list { +.list__watchlist { display: flex; flex-direction: column; } -.catalog-list-item { +.watchlist { display: flex; - flex-direction: row; + flex-direction: column; align-items: center; gap: 1.6rem; padding: 1.6rem 0; } -.catalog-list-item:not(:last-child) { + + +.watchlist:not(:last-child) { border-bottom: 1px solid rgba(0,0,0,.1); } -.posters { +.list__poster { display: flex; align-items: center; + position: relative; } .poster:not(:first-child) { margin-left: -2rem; } -.catalog-list-content { +.poster:hover { + z-index: 20!important; + top: -10px; +} + + +.watchlist__content { display: flex; flex-direction: column; - gap: 0.4rem; + gap: 0.8rem; flex-grow: 1; } -.catalog-list-content-title { +.watchlist__title { color: var(--accent-800); font-weight: 600; font-size: 1.3rem; } -.catalog-list-content-meta { +.watchlist__meta { display: flex; - gap: 0.2rem; - align-items: center; + flex-direction: column; + gap: 0.6rem; } -.catalog-list-content-type { +.watchlist__type { background-color: var(--accent-300); padding: 0.2rem 0.4rem; border-radius: 0.6rem; + max-width: fit-content; } .catalog-list-content-author { @@ -136,21 +139,38 @@ color: var(--accent-800); } -.catalog-list-content-description { - margin: 1rem 0; +.watchlist__dot { + display: none; +} + +.watchlist__date { + font-size: 0.9rem; + color: var(--slate-400); } -.catalog-list-content-count { - color: var(--accent-400); +.watchlist__description { + margin: 0.4rem 0; } -.catalog-list-actions { +.watchlist__item-count { display: flex; - flex-direction: column; align-items: center; - justify-content: center; + gap: 0.4rem; + color: var(--accent-500); +} + +.icon-clapperboard { + width: 1.2rem; +} + +.watchlist__actions { + display: flex; + flex-direction: row-reverse; + align-items: center; + justify-content: space-between; gap: 1rem; margin: 0 1rem; + width: 100%; } .catalog-list-btn { @@ -159,9 +179,32 @@ padding: 0; } -.container-btn-love { +.watchlist__action-love { display: flex; - flex-direction: column; align-items: center; justify-content: center; + gap: 0.4rem; +} + +@media screen and (min-width: 872px) { + .watchlist { + flex-direction: row; + } + .watchlist__actions { + flex-direction: column; + max-width: fit-content; + } + .watchlist__action-love { + flex-direction: column; + } +} + +@media screen and (min-width: 1024px) { + .filter { + flex-direction: row; + } + + .btn--apply { + max-width: fit-content; + } } \ No newline at end of file diff --git a/src/public/css/profile.css b/src/public/css/profile.css index 2ef5d14eb40a70985533a0ff56789d520d7321be..80d40823d2f3a0dfa335dba0889f852955d2c21c 100644 --- a/src/public/css/profile.css +++ b/src/public/css/profile.css @@ -9,7 +9,7 @@ padding-right: 40px; flex-direction: column; gap: 40px; - display: flex + display: flex; } .images { diff --git a/src/public/css/watchlistCreate.css b/src/public/css/watchlistCreate.css index b9a783459b8fcee158fec17f6786b8db12c7ef20..a1c8c087607a7299ffc9d29b5717039671b57816 100644 --- a/src/public/css/watchlistCreate.css +++ b/src/public/css/watchlistCreate.css @@ -1,26 +1,21 @@ -.container-watchlist-create { - padding: 0 1rem; - max-width: 80rem; - width: 100%; - margin: 3rem auto; - position: relative; -} - -.container-form-watchlist-create { +.container__create-watchlist { display: flex; + flex-direction: column; gap: 4rem; - margin: 2rem 0; } -.subcon-form-watchlist-create { +.container__form { width: 100%; flex-grow: 1; } -.form-watchlist-create { +.form__create-watchlist { max-width: 50rem; } +.search__title { + +} .form-watchlist-actions { display: flex; @@ -28,17 +23,27 @@ gap: 0.6rem; } -.container-watchlist-items { +.watchlist-items { display: flex; margin: 2rem 0; gap: 1rem; } -.btn-watchlist-add-item { +.actions { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + width: 100%; +} + +.btn__add-item { min-width: 18rem; } -.btn-watchlist-save { +.btn__save { min-width: 18rem; + width: 100%; } diff --git a/src/public/js/components/alert.js b/src/public/js/components/alert.js new file mode 100644 index 0000000000000000000000000000000000000000..b7ba965e7a8d4f882cd42202aad163eaaca98c3c --- /dev/null +++ b/src/public/js/components/alert.js @@ -0,0 +1,4 @@ +btnClose = document.querySelector(".alert button"); +btnClose.addEventListener("click", () => { + document.querySelector(".alert").remove(); +}); diff --git a/src/public/js/components/modal.js b/src/public/js/components/modal.js new file mode 100644 index 0000000000000000000000000000000000000000..1b16320bd1f83b8fbde72747b60463cda8241d8a --- /dev/null +++ b/src/public/js/components/modal.js @@ -0,0 +1,20 @@ +const modalTrigger = document.querySelector('.modal__trigger'); +const modalContentWrapper = document.querySelector('#modal__content'); +const containerDefault = document.querySelector('.container__default'); +const modalClose = document.querySelector('.modal__close'); + +modalTrigger.addEventListener('click', function () { + if (modalContentWrapper.style.display === 'flex'){ + modalContentWrapper.classList.style.display = 'none' + } else { + modalContentWrapper.style.display = 'flex'; + document.body.style.overflow = 'hidden'; + containerDefault.style.zIndex = '1000'; + } +}) + +modalClose.addEventListener('click', function () { + modalContentWrapper.style.display = 'none'; + document.body.style.overflow = 'unset'; + containerDefault.style.zIndex = '10'; +}) \ No newline at end of file diff --git a/src/public/js/components/modal/watchlistAddItem.js b/src/public/js/components/modal/watchlistAddItem.js new file mode 100644 index 0000000000000000000000000000000000000000..65264ded82d19bf6f2ecc0e871f404280f7c94c8 --- /dev/null +++ b/src/public/js/components/modal/watchlistAddItem.js @@ -0,0 +1,7 @@ +const inputSearch = document.querySelector('.search__input'); + +inputSearch.addEventListener('keydown', function (e) { + if (e.keyCode === 13) { + e.preventDefault(); + } +}) diff --git a/src/public/js/select.js b/src/public/js/components/select.js similarity index 90% rename from src/public/js/select.js rename to src/public/js/components/select.js index 5d3d7dae790f3fa8fdc78169eac7f6e347e90b7a..44d21c02438424f1d653112f9a191c6cc1f5322f 100644 --- a/src/public/js/select.js +++ b/src/public/js/components/select.js @@ -10,7 +10,7 @@ selects.forEach(select => { option.forEach(optn => { optn.addEventListener('click', (event) => { selectBtn.textContent = event.currentTarget.innerText; - input.value = event.currentTarget.innerText; + input.value = event.currentTarget.innerText.trim(); }) }) }) diff --git a/src/public/js/home.js b/src/public/js/home.js index 92f633312f3aa110b753e7bba8eb564eb11f5f93..921396ed0a4143fc393a04819148d74522452003 100644 --- a/src/public/js/home.js +++ b/src/public/js/home.js @@ -1,18 +1,16 @@ -const btnSort = document.querySelector('.btn-sort'); -const sortAsc = document.querySelector('.btn-sort-asc'); -const sortDesc = document.querySelector('.btn-sort-desc'); -const images = document.querySelectorAll('.poster'); -const order = document.querySelector('#order'); +const btnSort = document.querySelector(".btn-sort"); +const sortAsc = document.querySelector(".btn-sort-asc"); +const sortDesc = document.querySelector(".btn-sort-desc"); +const images = document.querySelectorAll(".poster"); +const order = document.querySelector("#order"); +const btnApply = document.querySelector("#btn-apply"); -btnSort.addEventListener('click', () => { - sortAsc.classList.toggle('hidden'); - sortDesc.classList.toggle('hidden'); - order.value = order.value == 'asc' ? 'desc' : 'asc'; - console.log(order.value); -}) +btnSort.addEventListener("click", () => { + sortAsc.classList.toggle("hidden"); + sortDesc.classList.toggle("hidden"); + order.value = order.value == "asc" ? "desc" : "asc"; +}); images.forEach((element, i) => { - element.style.zIndex = `${images.length - i}`; + element.style.zIndex = `${images.length - i}`; }); - - diff --git a/src/public/js/watchlistCreate.js b/src/public/js/watchlistCreate.js new file mode 100644 index 0000000000000000000000000000000000000000..ec47f41503ceb22661b35fd016368777cf7db40a --- /dev/null +++ b/src/public/js/watchlistCreate.js @@ -0,0 +1,17 @@ +const btnAddItem = document.querySelector('.btn__add-item'); +const watchlistItems = document.querySelector('.watchlist-items'); + +// function addItem () { +// const xhttp = new XMLHttpRequest(); +// xhttp.onreadystatechange = function () { +// if (this.readyState === 4) { +// const div = document.createElement('div'); +// div.innerHTML = this.response; +// watchlistItems.appendChild(div); +// } +// } +// xhttp.open("POST", "/cc/watchlist-item", true); +// xhttp.send(); +// } + +// btnAddItem.addEventListener('click', addItem); \ No newline at end of file diff --git a/src/seed/seed.sql b/src/seed/seed.sql index 933a9fc8e229ab4906f33f937e0c5aafcbd037a5..16eb223287e01bfa4ad06a7c549d668bf5af0e84 100644 --- a/src/seed/seed.sql +++ b/src/seed/seed.sql @@ -4,12 +4,6 @@ VALUES ('4d5e6f', 'Jane Smith', 'securepwd', 'jane@example.com', 'ADMIN', NOW(), NOW()), ('7g8h9i', 'Alice Johnson', 'pass123', 'alice@example.com', 'BASIC', NOW(), NOW()); -INSERT INTO sessions (user_id, created_at, updated_at) -VALUES - (1, NOW(), NOW()), - (2, NOW(), NOW()), - (3, NOW(), NOW()); - INSERT INTO catalogs (uuid, title, description, poster, trailer, category, created_at, updated_at) VALUES ('abc123', 'Anime Show 1', 'Description 1', 'poster1.jpg', 'trailer1.mp4', 'ANIME', NOW(), NOW()), diff --git a/src/server/app/App/Controller.php b/src/server/app/App/Controller.php deleted file mode 100644 index 6a4df5484a7a347d48d41f9d3ea1aa91a996261b..0000000000000000000000000000000000000000 --- a/src/server/app/App/Controller.php +++ /dev/null @@ -1,9 +0,0 @@ -<?php - -class Controller -{ - public function view(string $view, array $data = []): void - { - require_once __DIR__ . '/../View' . $view . '.php'; - } -} diff --git a/src/server/app/App/Domain.php b/src/server/app/App/Domain.php new file mode 100644 index 0000000000000000000000000000000000000000..4c683587033e16546d686ad06b7c665aa17378cf --- /dev/null +++ b/src/server/app/App/Domain.php @@ -0,0 +1,15 @@ +<?php + +abstract class Domain +{ + public string $table; + + public function __construct() + { + $this->table = get_class($this) . 's'; + } + + abstract public function toArray(): array; + + abstract public function fromArray(array $data); +} \ No newline at end of file diff --git a/src/server/app/Middleware/Middleware.php b/src/server/app/App/Middleware.php similarity index 52% rename from src/server/app/Middleware/Middleware.php rename to src/server/app/App/Middleware.php index 6f9e4131e661dff7e11c8b833e402470fb9b3182..10de9271143365a07e9f49692132943854cb5675 100644 --- a/src/server/app/Middleware/Middleware.php +++ b/src/server/app/App/Middleware.php @@ -2,5 +2,5 @@ interface Middleware { - function before(): void; + function run(): void; } diff --git a/src/server/app/App/Repository.php b/src/server/app/App/Repository.php new file mode 100644 index 0000000000000000000000000000000000000000..061c84ae84885cb8cfb81e676a28f2e0200ce526 --- /dev/null +++ b/src/server/app/App/Repository.php @@ -0,0 +1,225 @@ +<?php +require_once __DIR__ . '/../Utils/FilterBuilder.php'; + +/** + * ABC for Repository + * + * Provides basic CRUD operations + */ +abstract class Repository +{ + protected \PDO $connection; + protected string $table; + + public function __construct(\PDO $connection) + { + $this->connection = $connection; + } + + public function save(Domain $domain) + { + $domainKeyLength = count($domain->toArray()); + $query = "INSERT INTO {$this->table} ("; + + $countKey = 0; + foreach ($domain->toArray() as $key => $value) { + if ($key != 'id') { + $query .= "$key"; + } + + if ($countKey < $domainKeyLength - 1) { + $query .= ", "; + } + + $countKey += 1; + } + + $query .= ") VALUES ("; + $countKey = 0; + foreach ($domain->toArray() as $key => $value) { + if ($key != 'id') { + $query .= ":$key"; + } + + if ($countKey < $domainKeyLength - 1) { + $query .= ", "; + } + + $countKey += 1; + } + + $query .= ")"; + $statement = $this->connection->prepare($query); + foreach ($domain->toArray() as $key => $value) { + if ($key != 'id') { + $statement->bindValue(":$key", $value); + } + } + + $statement->execute(); + + try { + $domain->id = $this->connection->lastInsertId(); + return $domain; + } finally { + $statement->closeCursor(); + } + } + + public function findAll( + array $filter = [], + array $search = [], + array $projection = [], + int $page = 1, + int $pageSize = 10 + ): array { + $filterBuilder = new FilterBuilder(); + $query = ""; + $selectQuery = "SELECT "; + $pageCountQuery = "SELECT COUNT(*) "; + + if (count($projection) === 0) { + $selectQuery .= "*"; + } else { + $countProjection = 0; + foreach ($projection as $column) { + $selectQuery .= "$column"; + if ($countProjection < count($projection) - 1) { + $selectQuery .= ", "; + } + $countProjection += 1; + } + } + + $query .= " FROM {$this->table}"; + + $filterCount = 0; + foreach ($filter as $key => $value) { + if ($filterCount == 0) { + $filterBuilder->whereEquals($key, $value); + } else { + $filterBuilder->andWhereEquals($key, $value); + } + $filterCount += 1; + } + + foreach ($search as $key => $value) { + if ($filterCount == 0) { + $filterBuilder->whereContains($key, $value); + } else { + $filterBuilder->andWhereContains($key, $value); + } + $filterCount += 1; + } + + $query .= $filterBuilder->filterQuery; + + if ($pageSize) { + $query .= " LIMIT $pageSize"; + } + + if ($page) { + $offset = ($page - 1) * $pageSize; + $query .= " OFFSET $offset"; + } + + $selectStatement = $this->connection->prepare($selectQuery . $query); + $selectStatement->execute(); + + $pageCountStatement = $this->connection->prepare($pageCountQuery . $query); + $pageCountStatement->execute(); + + try { + return [ + 'items' => $selectStatement->fetchAll(), + 'page' => $page, + 'totalPage' => ceil($pageCountStatement->fetchColumn() / $pageSize) + ]; + } finally { + $selectStatement->closeCursor(); + } + } + + public function findOne($key, $value, $projection = []) + { + $query = "SELECT "; + + if (count($projection) === 0) { + $query .= "*"; + } else { + $countProjection = 0; + foreach ($projection as $column) { + $query .= "$column"; + if ($countProjection < count($projection) - 1) { + $query .= ", "; + } + $countProjection += 1; + } + } + + $query .= " FROM {$this->table} WHERE $key = :$key"; + $statement = $this->connection->prepare($query); + $statement->bindValue(":$key", $value); + + $statement->execute(); + + try { + if ($row = $statement->fetch()) { + return $row; + } + return null; + } finally { + $statement->closeCursor(); + } + } + + public function update(Domain $domain) + { + $domainKeyLength = count($domain->toArray()); + $query = "UPDATE {$this->table} SET "; + + $countKey = 0; + foreach ($domain->toArray() as $key => $value) { + if ($key != 'id') { + $query .= "$key = :$key"; + } + + if ($countKey < $domainKeyLength - 1) { + $query .= ", "; + } + + $countKey += 1; + } + + $query .= " WHERE id = :id"; + + $statement = $this->connection->prepare($query); + foreach ($domain->toArray() as $key => $value) { + if ($key != 'id') { + $statement->bindValue(":$key", $value); + } + } + + $statement->execute(); + + try { + return $domain; + } finally { + $statement->closeCursor(); + } + } + + public function deleteAll(): void + { + $this->connection->exec("DELETE FROM {$this->table}"); + } + + public function deleteBy($key, $value): void + { + $statement = $this->connection->prepare("DELETE FROM {$this->table} WHERE $key = :$key"); + $statement->bindValue(":$key", $value); + $statement->execute(); + + $statement->closeCursor(); + } +} \ No newline at end of file diff --git a/src/server/app/App/Router.php b/src/server/app/App/Router.php index c9ea1689bb38b85fa0579ce981f3bc7631b7e81b..b4b156d179075464e7c71dd73581eefa25e022a5 100644 --- a/src/server/app/App/Router.php +++ b/src/server/app/App/Router.php @@ -30,7 +30,7 @@ class Router // call middlewares foreach ($route['middlewares'] as $middleware) { $instance = new $middleware; - $instance->before(); + $instance->run(); } $function = $route['function']; @@ -44,6 +44,6 @@ class Router } http_response_code(404); - echo 'Controller not found 😔'; + echo 'Page Not Found 😔'; } } diff --git a/src/server/app/Controller/CatalogController.php b/src/server/app/Controller/CatalogController.php index c4ec316fbbb2f8e05a03c53d6a6b14dccecf221d..2bd2af949b496f95eef41f464cec10657f17cbfd 100644 --- a/src/server/app/Controller/CatalogController.php +++ b/src/server/app/Controller/CatalogController.php @@ -1,6 +1,5 @@ <?php -require_once __DIR__ . '/../App/Controller.php'; require_once __DIR__ . '/../App/View.php'; require_once __DIR__ . '/../Service/CatalogService.php'; require_once __DIR__ . '/../Repository/CatalogRepository.php'; @@ -22,13 +21,20 @@ class CatalogController public function index(): void { + $page = $_GET['page'] ?? 1; + $category = $_GET['category'] ?? "MIXED"; + View::render('catalog/index', [ 'title' => 'Catalog', 'styles' => [ + '/css/components/select.css', + '/css/components/button.css', + '/css/components/card.css', '/css/catalog.css', ], 'data' => [ - 'catalogs' => ['catalog1', 'catalog2', 'catalog3'] + 'catalogs' => $this->catalogService->findAll($page, $category), + 'category' => strtoupper(trim($category)) ] ]); } @@ -38,7 +44,10 @@ class CatalogController View::render('catalog/form', [ 'title' => 'Add Catalog', 'styles' => [ - '/css/form-catalog.css', + '/css/catalog-form.css', + '/css/components/select.css', + '/css/components/button.css', + '/css/components/input.css', ], ]); } @@ -48,52 +57,70 @@ class CatalogController View::render('catalog/form', [ 'title' => 'Edit Catalog', 'styles' => [ - '/css/form-catalog.css', + '/css/catalog-form.css', + '/css/components/select.css', + '/css/components/button.css', + '/css/components/input.css', ], ]); } public function detail(): void { + $uuid = '6517b94da6b8c'; + $catalog = $this->catalogService->findByUUID($uuid); + + if (!$catalog) { + View::render('catalog/not-found', [ + 'title' => 'Catalog Not Found', + 'styles' => [ + '/css/catalog-not-found.css', + ], + ]); + return; + } + View::render('catalog/detail', [ 'title' => 'Catalog Detail', 'styles' => [ '/css/catalog-detail.css', ], - 'data' => [ - 'title' => 'Snowdrop', - 'category' => 'ANIME', - 'description' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', - 'poster' => 'jihu-13.jpg', - 'trailer' => 'the-journey-of-elaina.mp4' - ] + 'data' => $catalog->toArray() ]); } public function postCreate(): void { $request = new CatalogCreateRequest(); - $request->category = $_POST['category']; + if (isset($_POST['category'])) { + $request->category = $_POST['category']; + } + $request->title = $_POST['title']; $request->description = $_POST['description']; - $request->poster = $_FILES['poster']; - if (isset($_FILES["trailer"]) && $_FILES["trailer"]["error"] === UPLOAD_ERR_OK) { - $request->trailer = $_FILES["trailer"]; + if (isset($_FILES['trailer'])) { + $request->trailer = $_FILES['trailer']; } try { $this->catalogService->create($request); View::redirect('/catalog'); } catch (ValidationException $exception) { - echo $exception->getMessage(); - View::render('catalog/create', [ - 'title' => 'Drawl | Add Catalog', + View::render('catalog/form', [ + 'title' => 'Add Catalog', 'error' => $exception->getMessage(), 'styles' => [ - '/css/catalog.css', + '/css/components/select.css', + '/css/components/button.css', + '/css/catalog-form.css', ], + 'data' => [ + 'title' => $request->title, + 'description' => $request->description, + 'category' => $request->category, + ] ]); } } diff --git a/src/server/app/Controller/HomeController.php b/src/server/app/Controller/HomeController.php index 7a934f3afadd42bf2c5f153721551035335f8589..6651137396abacec3542591d541aaa13c56c4de0 100644 --- a/src/server/app/Controller/HomeController.php +++ b/src/server/app/Controller/HomeController.php @@ -1,6 +1,4 @@ <?php - -require_once __DIR__ . '/../App/Controller.php'; require_once __DIR__ . '/../App/View.php'; require_once __DIR__ . '/../Config/Database.php'; @@ -28,10 +26,15 @@ class HomeController 'title' => 'Homepage', 'styles' => [ '/css/home.css', + '/css/components/select.css', + '/css/components/button.css', + '/css/components/card.css', + '/css/components/input.css', + '/css/components/icon.css', ], 'js' => [ '/js/home.js', ], ]); } -} +} \ No newline at end of file diff --git a/src/server/app/Controller/UserController.php b/src/server/app/Controller/UserController.php index fc6808349e0ec39a06b870bfc11edcefc30edbcd..dc6ea080715616dbc304f4389b56c2b50bb5f808 100644 --- a/src/server/app/Controller/UserController.php +++ b/src/server/app/Controller/UserController.php @@ -10,7 +10,7 @@ require_once __DIR__ . '/../Repository/SessionRepository.php'; require_once __DIR__ . '/../Service/UserService.php'; require_once __DIR__ . '/../Service/SessionService.php'; -require_once __DIR__ . '/../Model/UserRegisterRequest.php'; +require_once __DIR__ . '/../Model/UserSignUpRequest.php'; require_once __DIR__ . '/../Model/UserSignInRequest.php'; class UserController { @@ -30,7 +30,7 @@ class UserController public function signUp() { View::render('user/signUp', [ - 'title' => 'Drawl | Sign Up', + 'title' => 'Sign Up', 'styles' => [ '/css/signUp.css', ], @@ -41,7 +41,7 @@ class UserController public function showProfile() { View::render('user/profile', [ - 'title' => 'Drawl | Profile', + 'title' => 'Profile', 'styles' => [ '/css/profile.css', ], @@ -71,18 +71,17 @@ class UserController public function postSignUp() { - $request = new UserRegisterRequest(); - $request->id = $_POST['id']; - $request->name = $_POST['name']; + $request = new UserSignUpRequest(); + $request->email = $_POST['email']; $request->password = $_POST['password']; try { - $this->userService->register($request); + $this->userService->signUp($request); // redirect to login - View::redirect('/signIn'); + View::redirect('/signin'); } catch (ValidationException $exception) { View::render('user/signUp', [ - 'title' => 'Drawl | Sign Up', + 'title' => 'Sign Up', 'error' => $exception->getMessage(), 'styles' => [ '/css/signUp.css', @@ -94,7 +93,7 @@ class UserController public function signIn() { View::render('user/signIn', [ - "title" => "Drawl | Sign In", + "title" => "Sign In", "styles" => [ "/css/signIn.css", ], @@ -104,7 +103,7 @@ class UserController public function postSignIn() { $request = new UserSignInRequest(); - $request->id = $_POST['id']; + $request->email = $_POST['email']; $request->password = $_POST['password']; try { @@ -113,7 +112,7 @@ class UserController View::redirect('/'); } catch (ValidationException $exception) { View::render('user/signIn', [ - "title" => "Drawl | Sign In", + "title" => "Sign In", "error" => $exception->getMessage(), "styles" => [ "/css/signIn.css", diff --git a/src/server/app/Controller/WatchlistController.php b/src/server/app/Controller/WatchlistController.php index 75e774dd271c23d2b4eab37e47b9669f93e3f77f..ad71aee44fa4d48207c64716911be629c36eaefc 100644 --- a/src/server/app/Controller/WatchlistController.php +++ b/src/server/app/Controller/WatchlistController.php @@ -16,13 +16,15 @@ class WatchlistController public function create(): void { View::render('watchlist/create', [ - 'title' => 'Drawl | Create Watchlist', + 'title' => 'Create Watchlist', 'description' => 'Create new watchlist', 'styles' => [ '/css/watchlistCreate.css', + '/css/components/modal/watchlistAddItem.css', ], 'js' => [ '/js/watchlistCreate.js', + '/js/components/modal/watchlistAddItem.js', ] ]); } @@ -81,4 +83,9 @@ class WatchlistController ], ]); } + + public function watchlistItem() + { + return require __DIR__ . '/../View/components/watchlistItem.php'; + } } diff --git a/src/server/app/Domain/Catalog.php b/src/server/app/Domain/Catalog.php index 0240610d4713c22a01cc1a5a2ffcdebac19881eb..27d2618cd3841e6a2c125d511982b63e0c9d08a0 100644 --- a/src/server/app/Domain/Catalog.php +++ b/src/server/app/Domain/Catalog.php @@ -1,16 +1,61 @@ <?php -class Catalog +require_once __DIR__ . '/../App/Domain.php'; + +class Catalog extends Domain { public int $id; + public string $uuid; public string $title; public ?string $description = null; public string $poster; public ?string $trailer = null; public string $category; - public function __toString() + public function toArray(): array { - return "Catalog: {id: $this->id, title: $this->title, description: $this->description, poster: $this->poster, trailer: $this->trailer, category: $this->category}"; + return [ + 'uuid' => $this->uuid, + 'title' => $this->title, + 'description' => $this->description, + 'poster' => $this->poster, + 'trailer' => $this->trailer, + 'category' => $this->category + ]; + } + + public function fromArray(array $data) + { + if (isset($data['id'])) { + $this->id = $data['id']; + } + + if (isset($data['uuid'])) { + $this->uuid = $data['uuid']; + } + + if (isset($data['title'])) { + $this->title = $data['title']; + } + + if (isset($data['description'])) { + $this->description = $data['description']; + } + + if (isset($data['poster'])) { + if (file_exists('assets/images/catalogs/posters/' . $data['poster'])) { + $this->poster = $data['poster']; + } else { + $this->poster = 'no-poster.webp'; + } + } + + if (isset($data['trailer'])) { + $this->trailer = $data['trailer']; + } + + if (isset($data['category'])) { + $this->category = $data['category']; + } } } \ No newline at end of file diff --git a/src/server/app/Domain/Session.php b/src/server/app/Domain/Session.php index c90b5d95fd6db02d82ca20634999746d2b1a84c5..6afe9bfc2192f4abd22567609f365883c6403ebf 100644 --- a/src/server/app/Domain/Session.php +++ b/src/server/app/Domain/Session.php @@ -4,4 +4,5 @@ class Session { public string $id; public string $userId; + public string $expired; } diff --git a/src/server/app/Exception/FileUploaderException.php b/src/server/app/Exception/FileUploaderException.php new file mode 100644 index 0000000000000000000000000000000000000000..efe6106f0dc538ff63d643f509143d88fd545c43 --- /dev/null +++ b/src/server/app/Exception/FileUploaderException.php @@ -0,0 +1,6 @@ +<?php + +class FileUploaderException extends \Exception +{ + +} \ No newline at end of file diff --git a/src/server/app/Middleware/UserAuthMiddleware.php b/src/server/app/Middleware/UserAuthMiddleware.php new file mode 100644 index 0000000000000000000000000000000000000000..4d315698a7de96b94786a716e20dc983343dc334 --- /dev/null +++ b/src/server/app/Middleware/UserAuthMiddleware.php @@ -0,0 +1,29 @@ +<?php + +require_once __DIR__ . '/../App/Middleware.php'; +require_once __DIR__ . '/../Service/SessionService.php'; +require_once __DIR__ . '/../Config/Database.php'; + +require_once __DIR__ . '/../Repository/UserRepository.php'; +require_once __DIR__ . '/../Repository/UserRepository.php'; + +class UserAuthMiddleware implements Middleware +{ + private SessionService $sessionService; + + public function __construct() + { + $userRepository = new UserRepository(Database::getConnection()); + $sessionRepository = new SessionRepository(Database::getConnection()); + $this->sessionService = new SessionService($sessionRepository, $userRepository); + } + + public function run(): void + { + $user = $this->sessionService->current(); + if (!isset($user)) { + header("Location: /signin"); + exit(); + } + } +} diff --git a/src/server/app/Model/CatalogCreateRequest.php b/src/server/app/Model/CatalogCreateRequest.php index 7cf79313380cf181654b8e004bc6a4bdb77fe658..9ceb0f3fec592f76e970f195c11980c4414cf624 100644 --- a/src/server/app/Model/CatalogCreateRequest.php +++ b/src/server/app/Model/CatalogCreateRequest.php @@ -5,6 +5,6 @@ class CatalogCreateRequest public ?string $title = null; public ?string $description = null; public $poster = null; - public ?string $trailer = null; + public $trailer = null; public ?string $category = null; } \ No newline at end of file diff --git a/src/server/app/Model/UserSignInRequest.php b/src/server/app/Model/UserSignInRequest.php index e10389e963ca090f134acdc5b5a38e623b65f929..a1098ddd5275d1426724c36933e74ac3b2ea81e2 100644 --- a/src/server/app/Model/UserSignInRequest.php +++ b/src/server/app/Model/UserSignInRequest.php @@ -2,6 +2,6 @@ class UserSignInRequest { - public ?string $id = null; + public ?string $email = null; public ?string $password = null; } diff --git a/src/server/app/Model/UserRegisterRequest.php b/src/server/app/Model/UserSignUpRequest.php similarity index 77% rename from src/server/app/Model/UserRegisterRequest.php rename to src/server/app/Model/UserSignUpRequest.php index e637486424a762c0ceb6153d94f678cddbf088bf..1351b586df45e29aa530c999279386a2d3347027 100644 --- a/src/server/app/Model/UserRegisterRequest.php +++ b/src/server/app/Model/UserSignUpRequest.php @@ -1,11 +1,10 @@ <?php -class UserRegisterRequest +class UserSignUpRequest { - public ?int $id = null; public ?string $name = null; public ?string $password = null; public ?string $confirm_password = null; public ?string $email = null; public ?string $role = null; -} \ No newline at end of file +} diff --git a/src/server/app/Model/UserRegisterResponse.php b/src/server/app/Model/UserSignUpResponse.php similarity index 75% rename from src/server/app/Model/UserRegisterResponse.php rename to src/server/app/Model/UserSignUpResponse.php index 43a00e15611849d3d94dccf1edaf8a5d93e38712..443eb03643076dfc2ad352332b4eb834bb9ce755 100644 --- a/src/server/app/Model/UserRegisterResponse.php +++ b/src/server/app/Model/UserSignUpResponse.php @@ -2,7 +2,7 @@ require_once __DIR__ . '/../Domain/User.php'; -class UserRegisterResponse +class UserSignUpResponse { public User $user; } diff --git a/src/server/app/Repository/CatalogRepository.php b/src/server/app/Repository/CatalogRepository.php index 5d738526dff84a3d7ffb500d939a02cb52e277f4..a76ebe94dada8c22868b9cdfd5e3c7d5ce2dec97 100644 --- a/src/server/app/Repository/CatalogRepository.php +++ b/src/server/app/Repository/CatalogRepository.php @@ -1,55 +1,70 @@ <?php +require_once __DIR__ . '/../App/Repository.php'; require_once __DIR__ . '/../Domain/Catalog.php'; +require_once __DIR__ . '/../Utils/FilterBuilder.php'; -class CatalogRepository +class CatalogRepository extends Repository { - private \PDO $connection; + private FilterBuilder $filterBuilder; + protected string $table = 'catalogs'; public function __construct(\PDO $connection) { - $this->connection = $connection; + parent::__construct($connection); + $this->filterBuilder = new FilterBuilder(); } - public function save(Catalog $catalog): Catalog + public function update(Catalog $catalog): Catalog { - $statement = $this->connection->prepare("INSERT INTO catalogs(title, description, poster, trailer, category) VALUES (?, ?, ?, ?, ?)"); + $statement = $this->connection->prepare("UPDATE catalogs SET uuid = ?, title = ?, description = ?, poster = ?, trailer = ?, category = ? WHERE id = ?"); $statement->execute([ + $catalog->uuid, $catalog->title, $catalog->description, $catalog->poster, $catalog->trailer, $catalog->category, + $catalog->id ]); - return $catalog; - } - - public function findById(int $id): ?Catalog - { - $statement = $this->connection->prepare("SELECT id, title, description, poster, trailer, category FROM catalogs WHERE id = ?"); - $statement->execute([$id]); try { - if ($row = $statement->fetch()) { - $catalog = new Catalog(); - $catalog->id = $row['id']; - $catalog->title = $row['title']; - $catalog->description = $row['description']; - $catalog->poster = $row['poster']; - $catalog->trailer = $row['trailer']; - $catalog->category = $row['category']; - - return $catalog; - } else { - return null; - } + return $catalog; } finally { $statement->closeCursor(); } } - public function deleteAll(): void + public function findAll( + array $filter = [], + array $search = [], + array $projection = [], + int $page = 1, + int $pageSize = 10 + ): array { + $result = parent::findAll($filter, $search, $projection, $page, $pageSize); + + $result['items'] = array_map( + function ($row) { + $catalog = new Catalog(); + $catalog->fromArray($row); + return $catalog; + }, + $result['items'] + ); + return $result; + } + + public function findOne($key, $value, $projection = []): ?Catalog { - $this->connection->exec("DELETE FROM catalogs"); + $result = parent::findOne($key, $value, $projection); + + if ($result) { + $catalog = new Catalog(); + $catalog->fromArray($result); + return $catalog; + } else { + return null; + } } } \ No newline at end of file diff --git a/src/server/app/Repository/SessionRepository.php b/src/server/app/Repository/SessionRepository.php index a41a6af80ba0e8e61272f1ecf9e532ba430033d6..cd4aa9a889efc35e98cc29fdc5b03514f887deae 100644 --- a/src/server/app/Repository/SessionRepository.php +++ b/src/server/app/Repository/SessionRepository.php @@ -14,21 +14,24 @@ class SessionRepository public function save(Session $session): Session { - $statement = $this->connection->prepare("INSERT INTO sessions(id, user_id) VALUES (?, ?)"); - $statement->execute([$session->id, $session->userId]); + $statement = $this->connection->prepare("INSERT INTO sessions(id, user_id, expired) VALUES (?, ?, ?)"); + $statement->execute([$session->id, $session->userId, gmdate(DATE_RFC3339, strtotime("+1 week"))]); return $session; } public function findById(string $id): ?Session { - $statement = $this->connection->prepare("SELECT id, user_id FROM sessions WHERE id = ?"); + $statement = $this->connection->prepare("SELECT id, user_id, expired FROM sessions WHERE id = ?"); $statement->execute([$id]); try { if ($row = $statement->fetch()) { $session = new Session(); $session->id = $row['id']; - $session->userId = $row['userId']; + $session->userId = $row['user_id']; + $session->expired = $row['expired']; + + return $session; } else { return null; } @@ -47,4 +50,4 @@ class SessionRepository { $this->connection->exec("DELETE FROM sessions"); } -} +} \ No newline at end of file diff --git a/src/server/app/Repository/UserRepository.php b/src/server/app/Repository/UserRepository.php index 057d41d5cc6caf99b9426a3526047fd498924ffb..88aafba1cf2cea3d57ca35ea15bb9bf486b81d08 100644 --- a/src/server/app/Repository/UserRepository.php +++ b/src/server/app/Repository/UserRepository.php @@ -1,6 +1,7 @@ <?php require_once __DIR__ . '/../Domain/User.php'; +require_once __DIR__ . '/../Utils/UUIDGenerator.php'; class UserRepository { @@ -13,13 +14,12 @@ class UserRepository public function save(User $user): User { - $statement = $this->connection->prepare("INSERT INTO users(id, name, password, email, role) VALUES (?, ?, ?)"); + $statement = $this->connection->prepare("INSERT INTO users(uuid, name, password, email) VALUES (?, ?, ?, ?)"); $statement->execute([ - $user->id, + UUIDGenerator::uuid4(), $user->name, $user->password, $user->email, - $user->role, ]); return $user; } @@ -47,8 +47,31 @@ class UserRepository } } + 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(); + } + } + public function deleteAll(): void { $this->connection->exec("DELETE FROM users"); } -} \ No newline at end of file +} diff --git a/src/server/app/Service/CatalogService.php b/src/server/app/Service/CatalogService.php index 4ae9ced84fb8b88b3dd87dd2053c3622af2f1dd5..9e79fa51f79c318895bc980704df524e14a8c2eb 100644 --- a/src/server/app/Service/CatalogService.php +++ b/src/server/app/Service/CatalogService.php @@ -4,14 +4,46 @@ require_once __DIR__ . '/../Model/CatalogCreateRequest.php'; require_once __DIR__ . '/../Model/CatalogCreateResponse.php'; require_once __DIR__ . '/../Repository/CatalogRepository.php'; require_once __DIR__ . '/../Config/Database.php'; +require_once __DIR__ . '/../Utils/FileUploader.php'; class CatalogService { private CatalogRepository $catalogRepository; + private FileUploader $posterUploader; + private FileUploader $trailerUploader; public function __construct(CatalogRepository $catalogRepository) { $this->catalogRepository = $catalogRepository; + $this->posterUploader = new FileUploader('Poster', 'assets/images/catalogs/posters/'); + $this->trailerUploader = new FileUploader('Trailer', 'assets/videos/catalogs/trailers/'); + + $this->trailerUploader->allowedExtTypes = ["mp4"]; + $this->trailerUploader->allowedMimeTypes = ["video/mp4"]; + $this->trailerUploader->maxFileSize = 100000000; + } + + public function findAll(int $page = 1, string $category = "MIXED"): array + { + $filter = []; + if ($category != "MIXED") { + $filter['category'] = strtoupper(trim($category)); + } + + $projection = ['id', 'uuid', 'title', 'category', 'description', 'poster']; + $catalogs = $this->catalogRepository->findAll($filter, [], $projection, $page); + return $catalogs; + } + + public function findByUUID(string $uuid): ?Catalog + { + $catalog = $this->catalogRepository->findOne('uuid', $uuid); + return $catalog; + } + + public function deleteByUUID(string $uuid): void + { + $this->catalogRepository->deleteBy('uuid', $uuid); } public function create(CatalogCreateRequest $request): CatalogCreateResponse @@ -23,13 +55,18 @@ class CatalogService $catalog = new Catalog(); - $filename = $this->uploadFile($request->poster); - + $catalog->uuid = uniqid(); $catalog->title = $request->title; $catalog->description = $request->description; - $catalog->poster = $filename; - $catalog->trailer = $request->trailer ? $request->trailer['name'] : null; - $catalog->category = $request->category; + + $postername = $this->posterUploader->uploadFie($request->poster, $catalog->title); + if ($request->trailer && $request->trailer['error'] == UPLOAD_ERR_OK) { + $trailername = $this->trailerUploader->uploadFie($request->trailer, $catalog->title); + } + + $catalog->poster = $postername; + $catalog->trailer = $trailername ?? null; + $catalog->category = strtoupper(trim($request->category)); $this->catalogRepository->save($catalog); @@ -38,42 +75,29 @@ class CatalogService Database::commitTransaction(); return $response; + } catch (FileUploaderException $exception) { + Database::rollbackTransaction(); + throw new ValidationException($exception->getMessage()); } catch (\Exception $exception) { Database::rollbackTransaction(); throw $exception; } } - private function uploadFile($file): string - { - $filename = basename($file['name']); - $target_dir = "assets/images/"; - $target_file = $target_dir . $filename; - - // SANITIZE FILE - if (file_exists($target_file)) { - // echo "Sorry, file already exists."; - $filename = uniqid() . '-' . $filename; - $target_file = $target_dir . $filename; - } - - if (move_uploaded_file($file["tmp_name"], $target_file)) { - // echo "The file " . htmlspecialchars($filename) . " has been uploaded."; - } else { - // echo "Sorry, there was an error uploading your file."; - } - - return $filename; - } - private function validateCatalogCreateRequest(CatalogCreateRequest $request) { if ( $request->title == null || trim($request->title) == "" ) { - throw new ValidationException("Title, description cannot be blank"); + throw new ValidationException("Title cannot be blank."); } - // more validations goes here + if ($request->category == null || trim($request->category) == "") { + throw new ValidationException("Category cannot be blank."); + } + + if ($request->poster == null || $request->poster['error'] != UPLOAD_ERR_OK) { + throw new ValidationException("Poster cannot be blank."); + } } -} +} \ No newline at end of file diff --git a/src/server/app/Service/SessionService.php b/src/server/app/Service/SessionService.php index 85f2da0096f063491640d17dc04a42639c87ced6..0b75b419832b47911307f160eba98fe589f88172 100644 --- a/src/server/app/Service/SessionService.php +++ b/src/server/app/Service/SessionService.php @@ -4,10 +4,11 @@ require_once __DIR__ . '/../Repository/SessionRepository.php'; require_once __DIR__ . '/../Repository/UserRepository.php'; require_once __DIR__ . '/../Domain/Session.php'; require_once __DIR__ . '/../Domain/User.php'; +require_once __DIR__ . '/../Utils/UUIDGenerator.php'; class SessionService { - public static string $COOKIE_NAME = 'X-DRAWL-SESSION'; + public static string $COOKIE_NAME = 'bogoshipo__ohimesama'; private SessionRepository $sessionRepository; private UserRepository $userRepository; @@ -20,12 +21,12 @@ class SessionService public function create(string $userId): Session { $session = new Session(); - $session->id = uniqid(); + $session->id = UUIDGenerator::uuid4(); $session->userId = $userId; $this->sessionRepository->save($session); - setcookie(self::$COOKIE_NAME, $session->id, time() + (60 * 60 * 24), "/"); + setcookie(self::$COOKIE_NAME, $session->id, time() + (60 * 60 * 24 * 7), "/"); return $session; } @@ -41,7 +42,9 @@ class SessionService { $sessionId = $_COOKIE[self::$COOKIE_NAME] ?? ''; $session = $this->sessionRepository->findById($sessionId); - if ($session == null) { + + if ($session == null || $session->expired < gmdate(DATE_RFC3339)) { + $this->destroy(); return null; } diff --git a/src/server/app/Service/UserService.php b/src/server/app/Service/UserService.php index 4cda11576edaffcd3d5b21d16a0c6e41a9a142ce..4fe31f026c8a5b9d97e5cdba0c44baac772801c0 100644 --- a/src/server/app/Service/UserService.php +++ b/src/server/app/Service/UserService.php @@ -1,7 +1,7 @@ <?php -require_once __DIR__ . '/../Model/UserRegisterRequest.php'; -require_once __DIR__ . '/../Model/UserRegisterResponse.php'; +require_once __DIR__ . '/../Model/UserSignUpRequest.php'; +require_once __DIR__ . '/../Model/UserSignUpResponse.php'; require_once __DIR__ . '/../Model/UserSignInRequest.php'; require_once __DIR__ . '/../Model/UserSignInResponse.php'; @@ -17,24 +17,25 @@ class UserService $this->userRepository = $userRepository; } - public function register(UserRegisterRequest $request): UserRegisterResponse + public function signUp(UserSignUpRequest $request): UserSignUpResponse { - $this->validateUserRegistrationRequest($request); + $this->validateUserSignUpRequest($request); try { Database::beginTransaction(); - $user = $this->userRepository->findById($request->id); + $user = $this->userRepository->findByEmail($request->email); if ($user != null) { throw new ValidationException("User already exist"); } $user = new User(); - $user->name = $request->name; + $user->name = explode("@", $request->email)[0]; + $user->email = $request->email; $user->password = password_hash($request->password, PASSWORD_BCRYPT); $this->userRepository->save($user); - $response = new UserRegisterResponse(); + $response = new UserSignUpResponse(); $response->user = $user; Database::commitTransaction(); @@ -45,13 +46,13 @@ class UserService } } - private function validateUserRegistrationRequest(UserRegisterRequest $request) + private function validateUserSignUpRequest(UserSignUpRequest $request) { if ( - $request->name == null | $request->password == null || - trim($request->name) == "" || trim($request->password) == "" + $request->email == null | $request->password == null || + trim($request->email) == "" || trim($request->password) == "" ) { - throw new ValidationException("Id, name, password cannot be blank"); + throw new ValidationException("Email and password cannot be blank"); } // more validations goes here @@ -61,9 +62,9 @@ class UserService { $this->validateUserSignInRequest($request); - $user = $this->userRepository->findById($request->id); + $user = $this->userRepository->findByEmail($request->email); if ($user == null) { - throw new ValidationException("Id or password not valid"); + throw new ValidationException("Email or password not valid"); } if (password_verify($request->password, $user->password)) { @@ -71,17 +72,17 @@ class UserService $response->user = $user; return $response; } else { - throw new ValidationException("Id or password not valid"); + throw new ValidationException("Email or password not valid"); } } private function validateUserSignInRequest(UserSignInRequest $request) { if ( - $request->id == null || $request->password == null || - trim($request->id) == "" || trim($request->password) == "" + $request->email == null || $request->password == null || + trim($request->email) == "" || trim($request->password) == "" ) { - throw new ValidationException("Id and password cannot be blank"); + throw new ValidationException("Email and password cannot be blank"); } // more validations goes here diff --git a/src/server/app/Utils/FileUploader.php b/src/server/app/Utils/FileUploader.php new file mode 100644 index 0000000000000000000000000000000000000000..83f5f79a9239959fd338449febef0d94b484c59a --- /dev/null +++ b/src/server/app/Utils/FileUploader.php @@ -0,0 +1,76 @@ +<?php + +require_once __DIR__ . '/../Exception/FileUploaderException.php'; + +class FileUploader +{ + public string $id; + public int $maxFilenameSize = 255; + public int $maxFileSize = 100000; + public string $targetDir = '.'; + public array $allowedExtTypes = ["jpg", "jpeg", "png", "webp"]; + public array $allowedMimeTypes = ["image/jpeg", "image/png"]; + + public function __construct($id, $targetDir) + { + $this->id = $id; + $this->targetDir = $targetDir; + } + public function uploadFie($file, $filename) + { + if ($file == null) { + throw new FileUploaderException($this->id . " File is required."); + } + + $this->validateFile($file); + $isImage = in_array("image/jpeg", $this->allowedMimeTypes) || in_array("image/png", $this->allowedMimeTypes); + if ($isImage) { + $fileext = ".webp"; + } else { + $fileext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + } + $filename = uniqid() . '_' . $filename . '.' . $fileext; + $targetFile = $this->targetDir . $filename; + + if (file_exists($targetFile)) { + throw new FileUploaderException($this->id . " File already exists."); + } + + + if (move_uploaded_file($file["tmp_name"], $targetFile)) { + return $filename; + } + throw new FileUploaderException($this->id . " File is not a valid upload file."); + } + + private function validateFile($file) + { + $this->checkFileUploadedExt($file); + $this->checkFileUploadedSize($file); + $this->checkFileUploadedMimeType($file); + } + + private function checkFileUploadedExt($file) + { + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if (!in_array($ext, $this->allowedExtTypes)) { + throw new FileUploaderException($this->id . " File does not have a valid extension. Only allowed " . implode(", ", $this->allowedExtTypes) . "."); + } + } + + private function checkFileUploadedSize($file) + { + if ($file["size"] > $this->maxFileSize) { + throw new FileUploaderException($this->id . " File is too big. Maximum " . $this->maxFileSize . " bytes."); + } + } + + private function checkFileUploadedMimeType($file) + { + $mimeType = $file['type']; + + if (!in_array($mimeType, $this->allowedMimeTypes)) { + throw new FileUploaderException($this->id . " File does not have a valid mime type."); + } + } +} \ No newline at end of file diff --git a/src/server/app/Utils/FilterBuilder.php b/src/server/app/Utils/FilterBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..7fa8b9771ad034f3e374ae52130014be6a802e26 --- /dev/null +++ b/src/server/app/Utils/FilterBuilder.php @@ -0,0 +1,47 @@ +<?php + +class FilterBuilder +{ + public string $filterQuery; + + public function __construct() + { + $this->filterQuery = ""; + } + + public function whereEquals(string $key, $value): FilterBuilder + { + $this->filterQuery .= " WHERE $key = '$value'"; + return $this; + } + + public function whereContains(string $key, $value): FilterBuilder + { + $this->filterQuery .= " WHERE $key LIKE '%$value%'"; + return $this; + } + + public function andWhereEquals(string $key, $value): FilterBuilder + { + $this->filterQuery .= " AND "; + return $this->whereEquals($key, $value); + } + + public function andWhereContains(string $key, $value): FilterBuilder + { + $this->filterQuery .= " AND "; + return $this->whereContains($key, $value); + } + + public function orWhereEquals(string $key, $value): FilterBuilder + { + $this->filterQuery .= " OR "; + return $this->whereEquals($key, $value); + } + + public function orWhereContains(string $key, $value): FilterBuilder + { + $this->filterQuery .= " OR "; + return $this->whereContains($key, $value); + } +} \ No newline at end of file diff --git a/src/server/app/Utils/UUIDGenerator.php b/src/server/app/Utils/UUIDGenerator.php new file mode 100644 index 0000000000000000000000000000000000000000..228edd8bbd0bda8b74ab60773de542859bdb3a4e --- /dev/null +++ b/src/server/app/Utils/UUIDGenerator.php @@ -0,0 +1,14 @@ +<?php + +class UUIDGenerator +{ + public static function uuid(int $half = 16) + { + return bin2hex(random_bytes($half)); + } + + public static function uuid4(): string + { + return vsprintf('%s%s%s%s', str_split(self::uuid(8), 4)); + } +} diff --git a/src/server/app/View/catalog/detail.php b/src/server/app/View/catalog/detail.php index 4d81fb17881d14c367b6996e2c39b6237ab95588..83fd6c53ad0cabf2863a807629130814846e9b9f 100644 --- a/src/server/app/View/catalog/detail.php +++ b/src/server/app/View/catalog/detail.php @@ -2,7 +2,7 @@ $data = $model['data'] ?> -<main class="container container-catalog-detail"> +<main> <article class="catalog-detail-header"> <div class="catalog-detail-header-poster"></div> <img class="catalog-poster" src="<?= '/assets/images/catalogs/posters/' . $data['poster'] ?>" diff --git a/src/server/app/View/catalog/form.php b/src/server/app/View/catalog/form.php index 03fdd4243a72520fc0506d7205e083c8054291f8..39250e02b0a67e16cb1769346fcab853557ec965 100644 --- a/src/server/app/View/catalog/form.php +++ b/src/server/app/View/catalog/form.php @@ -1,7 +1,7 @@ <?php function selectCategory() { - $id = 'type'; - $placeholder = 'Select Type'; + $id = 'category'; + $placeholder = 'Select Category'; $content = [ "Drama", "Anime" @@ -24,40 +24,50 @@ function pagination() $totalPage = 10; require __DIR__ . '/../components/pagination.php'; } + +function alert($title, $message) +{ + $type = 'error'; + require __DIR__ . '/../components/alert.php'; +} ?> -<div class="container__default"> +<main> <h2> <?= $model['title'] ?> </h2> + <?php if (isset($model['error'])): ?> + <?php alert('Failed to ' . $model['title'], $model['error']); ?> + <?php endif; ?> <form action="/catalog/create" method="POST" enctype="multipart/form-data"> <div class="input-group"> - <label class="input-required">Type</label> + <label class="input-required">Category</label> <?php selectCategory(); ?> </div> <div class="input-group"> <label for="titleField" class="input-required">Title</label> - <input type="text" id="titleField" name="title" placeholder="Title" value="<?php $model['data']['title'] ?? "" ?>" maxlength="40" required> + <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 name="description" id="descriptionField" value="<?php $model['data']['description'] ?? "" ?>" maxlength="255"></textarea> + <textarea name="description" id="descriptionField" value="<?= $model['data']['description'] ?? "" ?>" + maxlength="255"></textarea> </div> <div class="input-group"> - <label for="posterField" class="input-required">Poster (Max 200MB)</label> + <label for="posterField" class="input-required">Poster</label> <input type="file" id="posterField" name="poster" accept="image/*" required> </div> <div class="input-group"> - <label for="videoField">Video (Max 30 seconds)</label> - <input type="file" id="videoField" name="video" accept="video/*"> + <label for="trailerField">Trailer</label> + <input type="file" id="trailerField" name="trailer" accept="trailer/mp4"> </div> <button class="btn-bold" type="submit"> Submit </button> </form> - -</div> \ No newline at end of file +</main> \ No newline at end of file diff --git a/src/server/app/View/catalog/index.php b/src/server/app/View/catalog/index.php index 58f5418a574acf3f9373860d482cab3ef388c5be..b8c4989429147221b61c1d56c38b9ac73826513b 100644 --- a/src/server/app/View/catalog/index.php +++ b/src/server/app/View/catalog/index.php @@ -1,38 +1,37 @@ -<?php function selectCategory() +<?php function selectCategory($selected) { - $id = 'type'; - $placeholder = 'Mixed'; + $id = 'category'; + $placeholder = 'MIXED'; $content = [ - "Mixed", - "Drama", - "Anime" + "MIXED", + "DRAMA", + "ANIME" ]; require __DIR__ . '/../components/select.php'; } -function catalogCard() +function catalogCard(Catalog $catalog) { $editable = true; - $title = 'Snowdrop'; - $poster = 'jihu-7.jpg'; - $category = 'DRAMA'; - $description = "Looking for a new animal companion, but tired of the same ol' cats and dogs? Here are some manga featuring unusual creatures that you'd never expect to see as pets!"; + $title = $catalog->title; + $poster = $catalog->poster; + $category = $catalog->category; + $description = $catalog->description; + $uuid = $catalog->uuid; require __DIR__ . '/../components/card/catalogCard.php'; } -function pagination() +function pagination(int $currentPage, int $totalPage) { - $currentPage = 7; - $totalPage = 10; require __DIR__ . '/../components/pagination.php'; } ?> -<div class="container__default"> +<main> <article class="search-filter"> - <form> + <form action="/catalog"> <div class="input"> - <label>Type</label> - <?php selectCategory(); ?> + <label>Category</label> + <?php selectCategory($model['data']['category'] ?? ""); ?> </div> <button class="btn-primary" type="submit"> Apply @@ -46,9 +45,9 @@ function pagination() </a> </article> <article class="content"> - <?php foreach ($model['data']['catalogs'] ?? [] as $catalog) : ?> - <?php catalogCard(); ?> + <?php foreach ($model['data']['catalogs']['items'] ?? [] as $catalog): ?> + <?php catalogCard($catalog); ?> <?php endforeach; ?> - <?php pagination(); ?> + <?php pagination($model['data']['catalogs']['page'], $model['data']['catalogs']['totalPage']); ?> </article> - </main> \ No newline at end of file +</main> \ No newline at end of file diff --git a/src/server/app/View/components/alert.php b/src/server/app/View/components/alert.php new file mode 100644 index 0000000000000000000000000000000000000000..4cc4bdb6064465c40e9e8e166bb364dbe6f02cb0 --- /dev/null +++ b/src/server/app/View/components/alert.php @@ -0,0 +1,14 @@ +<div class="alert" data-type="<?= $type ?>"> + <?php require PUBLIC_PATH . "assets/icons/" . $type . ".php" ?> + <div> + <h3> + <?= $title ?> + </h3> + <p> + <?= $message ?> + </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/components/card/catalogCard.php b/src/server/app/View/components/card/catalogCard.php index 126a25b671c2474893b95bb008972889d1e48f6e..7eb8f58e7b2785d03afa769842d26244eca6f689 100644 --- a/src/server/app/View/components/card/catalogCard.php +++ b/src/server/app/View/components/card/catalogCard.php @@ -8,9 +8,11 @@ <div class="card-content"> <img src=<?= "/assets/images/catalogs/posters/" . $poster ?> alt=<?= $title ?> class="poster" /> <div class="card-body"> - <h3> - <?= $title ?> - </h3> + <a href="/catalog/<?= $uuid ?>"> + <h3> + <?= $title ?> + </h3> + </a> <div class="tag"> <?= $category ?> </div> diff --git a/src/server/app/View/components/header.php b/src/server/app/View/components/header.php index f31e9048a3cbda52f760cc89aec6f86306bddbc2..12dbfe1058fbc2e92eb1cc1a151ac55944636e93 100644 --- a/src/server/app/View/components/header.php +++ b/src/server/app/View/components/header.php @@ -11,26 +11,30 @@ </title> <!-- CSS --> - <link rel='stylesheet' href='/css/global.css'> - <link rel='stylesheet' href='/css/components/select.css'> - <link rel='stylesheet' href='/css/components/card.css'> - <link rel='stylesheet' href='/css/components/button.css'> - <link rel='stylesheet' href='/css/components/tag.css'> - <link rel='stylesheet' href='/css/components/pagination.css'> - <link rel='stylesheet' href='/css/components/input.css'> - <link rel='stylesheet' href='/css/components/form.css'> - <link rel='stylesheet' href='/css/components/icon.css'> + <link rel="stylesheet" href="/css/global.css"> + <!-- <link rel="stylesheet" href="/css/components/select.css"> + <link rel="stylesheet" href="/css/components/card.css"> + <link rel="stylesheet" href="/css/components/button.css"> + <link rel="stylesheet" href="/css/components/pagination.css"> + <link rel="stylesheet" href="/css/components/input.css"> + <link rel="stylesheet" href="/css/components/form.css"> + <link rel="stylesheet" href="/css/components/icon.css"> + <link rel="stylesheet" href="/css/components/textarea.css"> + <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; ?> <!-- JS --> - <?php foreach ($model['js'] ?? [] as $js) : ?> + <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> + + <?php foreach ($model['js'] ?? [] as $js): ?> <script type='text/javascript' src='<?= $js ?>' defer></script> <?php endforeach; ?> - <script type='text/javascript' src='/js/select.js' defer></script> </head> <body> \ No newline at end of file diff --git a/src/server/app/View/components/modal.php b/src/server/app/View/components/modal.php new file mode 100644 index 0000000000000000000000000000000000000000..c05d92a683dfd5ad02ed929c0b07acd61329c997 --- /dev/null +++ b/src/server/app/View/components/modal.php @@ -0,0 +1,34 @@ +<!-- +Attributes: +- triggerClasses +- triggerText +- triggerIcon +- title +- content +--> + +<div class="modal"> + <button class="modal__trigger <?= $triggerClasses ?>"> + <?php + if (isset($triggerIcon) && $triggerIcon !== '') { + require PUBLIC_PATH . 'assets/icons/' . $triggerIcon . '.php'; + } + ?> + <?= $triggerText ?> + </button> + + <div id="modal__content" class="modal__backdrop"> + <div class="modal__content"> + <h2><?= $title ?></h2> + <button class="modal__close btn-ghost"> + <?php require PUBLIC_PATH . 'assets/icons/x.php' ?> + </button> + + <?php + if (isset($content)) { + require __DIR__ . '/../components/modal/' . $content . '.php'; + } + ?> + </div> + </div> +</div> \ No newline at end of file diff --git a/src/server/app/View/components/modal/watchlistAddItem.php b/src/server/app/View/components/modal/watchlistAddItem.php new file mode 100644 index 0000000000000000000000000000000000000000..bbb84046fb221f648cf87d95e15998b4071bcbd0 --- /dev/null +++ b/src/server/app/View/components/modal/watchlistAddItem.php @@ -0,0 +1,10 @@ +<div> + <form class="form-default form-search"> + <div class="search"> + <label> + <?php require PUBLIC_PATH . '/assets/icons/search.php' ?> + </label> + <input type="text" id="search" name="search" placeholder="Search by title" class="input-default search__input" /> + </div> + </form> +</div> \ No newline at end of file diff --git a/src/server/app/View/components/pagination.php b/src/server/app/View/components/pagination.php index 5313915a6f80b038f6f34bb4908a00b6471a6ee7..26e94aae821e6a5c5df2b9afffbbeeeb8152a0e5 100644 --- a/src/server/app/View/components/pagination.php +++ b/src/server/app/View/components/pagination.php @@ -1,6 +1,15 @@ +<?php +function createQueryUrl(int $page): string +{ + $currentQueryParams = $_GET; + $query = array_merge($currentQueryParams, ['page' => $page]); + $url = '?' . http_build_query($query); + return $url; +} +?> <div class="pagination"> <?php if ($currentPage > 1): ?> - <a href="?page=<?= $currentPage - 1 ?>" class="pagination-item prev"> + <a href="<?= createQueryUrl($currentPage - 1) ?>" class="pagination-item prev"> <?php require PUBLIC_PATH . 'assets/icons/chevron-down.php' ?> </a> <?php endif; ?> @@ -8,7 +17,7 @@ <span class="pagination-elips">...</span> <?php endif; ?> <?php for ($i = max(0, $currentPage - 2); $i < min($totalPage, $currentPage + 2); $i++): ?> - <a href="?page=<?= $i + 1 ?>" class="pagination-item" + <a href="<?= createQueryUrl($i + 1) ?>" class="pagination-item" data-type="<?= $i + 1 === $currentPage ? 'active' : 'inactive' ?>"> <?= $i + 1 ?> </a> @@ -17,7 +26,7 @@ <span class="pagination-elips">...</span> <?php endif; ?> <?php if ($currentPage < $totalPage): ?> - <a href="?page=<?= $currentPage + 1 ?>" class="pagination-item next"> + <a href="<?= createQueryUrl($currentPage + 1) ?>" class="pagination-item next"> <?php require PUBLIC_PATH . 'assets/icons/chevron-down.php' ?> </a> <?php endif; ?> diff --git a/src/server/app/View/components/select.php b/src/server/app/View/components/select.php index a4abd6dfb0456aa613a20914f05c8e17c6b74bcc..f94511d51a882209194fc4c940d78bdae343a3d0 100644 --- a/src/server/app/View/components/select.php +++ b/src/server/app/View/components/select.php @@ -2,28 +2,16 @@ - id - placeholder - content +- selected --> -<?php -if (!function_exists('validateQueryParams')) { - function validateQueryParams($id, $content): ?string - { - if (!isset($content)) return null; - if (isset($_GET[$id]) && in_array($_GET[$id], $content, TRUE)) { - return $_GET[$id]; - } - return null; - } -} -?> - <div class="c-select-menu" id="<?= $id ?>"> <div class="c-select-btn"> - <span class="c-select-btn-text"><?= validateQueryParams($id, $content) ?? $placeholder ?? 'Select' ?></span> + <span class="c-select-btn-text"><?= $selected ?? $placeholder ?? 'Select' ?></span> <?php require PUBLIC_PATH . 'assets/icons/chevron-down.php' ?> </div> - <input type="hidden" id="<?= $id ?>" name="<?= $id ?>" value="<?= validateQueryParams($id, $content) ?>" /> + <input type="hidden" id="<?= $id ?>" name="<?= $id ?>" value="<?= $selected ?>" /> <?php if (isset($content)) : ?> <ul class="c-select-options c-select-hide"> diff --git a/src/server/app/View/components/watchlistItem.php b/src/server/app/View/components/watchlistItem.php new file mode 100644 index 0000000000000000000000000000000000000000..be8c667bfab1c1430440938a342f031c6087c3ce --- /dev/null +++ b/src/server/app/View/components/watchlistItem.php @@ -0,0 +1 @@ +<p>Test</p> \ 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 4e0288c5feaada86692785e82c5c95f29b620dab..0ff71cfafda42723d96fc3abdeca9b2e0ce91d21 100644 --- a/src/server/app/View/home/index.php +++ b/src/server/app/View/home/index.php @@ -8,6 +8,7 @@ function selectCategory() "Drama", "Anime" ]; + $selected = validateQueryParams($id, $content); require __DIR__ . '/../components/select.php'; } ?> @@ -21,6 +22,7 @@ function sortBy() "Date", "Vote" ]; + $selected = validateQueryParams($id, $content); require __DIR__ . '/../components/select.php'; } ?> @@ -28,16 +30,18 @@ function sortBy() <?php function vallidateOrder(): ?string { - if (!isset($_GET["order"]) || ($_GET["order"] != "asc" && $_GET["order"] != "desc")) return null; + if (!isset($_GET["order"]) || ($_GET["order"] != "asc" && $_GET["order"] != "desc")) + return null; return $_GET["order"]; } ?> -<div class="container__default"> +<main> <form class="form-search-filter"> <div class="search"> <?php require PUBLIC_PATH . 'assets/icons/search.php'; ?> - <input type="text" name="search" placeholder="Search title, author" class="input-default input-search" value="<?= trim($_GET['search'] ?? '') ?? '' ?>" /> + <input type="text" name="search" placeholder="Search title, author" class="input-default input-search" + value="<?= trim($_GET['search'] ?? '') ?? '' ?>" /> </div> <div class="filter"> <?php selectCategory(); ?> @@ -47,52 +51,58 @@ function vallidateOrder(): ?string <span class="span-icon btn-sort-asc <?= vallidateOrder() == 'desc' ? 'hidden' : '' ?>"> <?php require PUBLIC_PATH . 'assets/icons/asc.php' ?> </span> - <span class="span-icon btn-sort-desc <?= vallidateOrder() == 'asc' || !vallidateOrder() ? 'hidden' : '' ?>"> + <span + class="span-icon btn-sort-desc <?= vallidateOrder() == 'asc' || !vallidateOrder() ? 'hidden' : '' ?>"> <?php require PUBLIC_PATH . 'assets/icons/desc.php' ?> </span> </button> <input type="hidden" id="order" name="order" value="<?= vallidateOrder() ?? 'asc' ?>" /> </div> </div> - <button type="submit" class="btn-primary btn--apply">Apply</button> + <button type="submit" id="btn-apply" class="btn-primary btn--apply">Apply</button> </form> - <a class="btn-primary" href='/watchlist/create'> + <a class="btn btn-primary" href='/watchlist/create'> <?php require PUBLIC_PATH . 'assets/icons/plus.php' ?> New List </a> - <div class="catalog-list"> - <?php for ($i = 0; $i < 5; $i++) : ?> - <div class="catalog-list-item"> - <div class="posters"> - <img src="./assets/images/jihu-13.jpg" alt="top-1" class="poster" /> - <img src="./assets/images/jihu-14.jpg" alt="top-2" class="poster" /> - <img src="./assets/images/jihu-15.jpg" alt="top-3" class="poster" /> - <img src="./assets/images/jihu-16.jpg" alt="top-4" class="poster" /> + <div class="list__watchlist"> + <?php for ($i = 0; $i < 5; $i++): ?> + <div class="watchlist"> + <div class="list__poster"> + <img src="./assets/images/catalogs/posters/6517b94da6c07_Elaina.webp" alt="top-1" class="poster" /> + <!-- <img src="./assets/images/catalogs/posters/6517b8f0bd688_Tomorrow.webp" alt="top-2" class="poster" /> + <img src="./assets/images/catalogs/posters/6517b94da6c07_Elaina.webp" alt="top-3" class="poster" /> + <img src="./assets/images/catalogs/posters/6517b8128a6c3_Tomorrow.webp" alt="top-4" class="poster" /> --> </div> - <div class="catalog-list-content"> - <h3 class="catalog-list-content-title">Best Anime for FURY, INCEST, and YURI</h3> - <div class="catalog-list-content-meta"> - <span class="catalog-list-content-type">anime</span> - <span class="catalog-list-content-author">by <span class="author-name">gamjaishite</span></span> - <span> + <div class="watchlist__content"> + <h3 class="watchlist__title">Best Anime for FURY, INCEST, and YURI</h3> + <div class="watchlist__meta"> + <div class="watchlist__wrapper-type-author"> + <span class="watchlist__type">anime</span> + <span class="catalog-list-content-author">by <span class="author-name">gamjaishite</span></span> + </div> + <span class="span-icon watchlist__dot"> <?php require PUBLIC_PATH . 'assets/icons/dot.php' ?> </span> - <span class="catalog-list-content-date">2 days ago</span> + <span class="watchlist__date">2 days ago</span> </div> - <p class="catalog-list-content-description"> + <p class="watchlist__description"> Looking for a new animal companion, but tired of the same ol' cats and dogs? Here are some manga featuring unusual creatures that you'd never expect to see as pets! </p> - <span class="catalog-list-content-count">20 items</span> + <span class="watchlist__item-count"> + <?php require PUBLIC_PATH . 'assets/icons/clapperboard.php' ?> + 20 items + </span> </div> - <div class="catalog-list-actions"> + <div class="watchlist__actions"> <button class="catalog-list-btn catalog-list-btn-save" type="button"> <?php require PUBLIC_PATH . 'assets/icons/bookmark.php' ?> </button> - <div class="container-btn-love"> + <div class="watchlist__action-love"> <button class="catalog-list-btn catalog-list-btn-love" type="button"> <?php require PUBLIC_PATH . 'assets/icons/love.php' ?> </button> @@ -102,4 +112,4 @@ function vallidateOrder(): ?string </div> <?php endfor; ?> </div> -</div> \ No newline at end of file +</main> \ No newline at end of file diff --git a/src/server/app/View/user/editProfile.php b/src/server/app/View/user/editProfile.php index 112a5f4af8ece6549b5fcaa09aa9b7992e32e20a..9ae8d881578a39f09824cfd3138639a229a86248 100644 --- a/src/server/app/View/user/editProfile.php +++ b/src/server/app/View/user/editProfile.php @@ -1,4 +1,4 @@ -<div class="container__default"> +<main> <div class="edit-parameters"> <div class="my-profile-container"> <h2>My Profile</h2> @@ -57,4 +57,4 @@ </div> </div> </div> -</div> \ No newline at end of file +</main> \ No newline at end of file diff --git a/src/server/app/View/user/profile.php b/src/server/app/View/user/profile.php index 248588f36756d65ad4ed884c9bcc35aaf4fbbeb4..ee276ee76168ebd4de093b23356e93b2b634c3c0 100644 --- a/src/server/app/View/user/profile.php +++ b/src/server/app/View/user/profile.php @@ -25,13 +25,15 @@ <?= $model['data']['desc'] ?> </p> </div> - <div class="positioning"> - <a href="/editProfile"> - <button class="btn-icon"> - <?php require PUBLIC_PATH . 'assets/icons/edit.php' ?> - </button> - </a> - + <div class="edit positioning"> + <button class="edit button"> + <?php require PUBLIC_PATH . 'assets/icons/edit.php' ?> + </button> + </div> + <div class="delete positioning"> + <button class="delete button"> + <?php require PUBLIC_PATH . 'assets/icons/delete.php' ?> + </button> </div> </div> </div> diff --git a/src/server/app/View/user/signIn.php b/src/server/app/View/user/signIn.php index c12fd638dd18637acb71c8f7c5ec1ef84ce538f8..ae8a7c9094d8d7b082ae6406c016d35bf77b95a9 100644 --- a/src/server/app/View/user/signIn.php +++ b/src/server/app/View/user/signIn.php @@ -1,23 +1,25 @@ -<div class="container container-signin"> +<main class="container-signin"> <h2>Sign In</h2> - <?php if (isset($model['error'])) {?> + <?php if (isset($model['error'])) { ?> <div class="alert-error alert-error-signin"> <p> - <?=$model['error']?> + <?= $model['error'] ?> </p> </div> - <?php }?> + <?php } ?> <div class="div-form-signin"> <form class="form-default" method="post" action='/signin'> <div class="form-input-default"> - <label for='id'>Id</label> - <input type="text" name='id' id='id' placeholder="Enter your id" class="input-default" value="<?=$_POST['id'] ?? ''?>"/> + <label for="email">Email</label> + <input type="text" name="email" id="email" placeholder="Enter your email" class="input-default" + value="<?= $_POST["email"] ?? '' ?>" /> </div> <div class="form-input-default"> - <label for='password'>Password</label> - <input type="password" name='password' id='password' placeholder="Enter your password" class="input-default"/> + <label for="password">Password</label> + <input type="password" name="password" id='password' placeholder="Enter your password" + class="input-default" /> </div> <button type="submit" class="btn-primary">Sign In</button> </form> </div> -</div> +</main> \ 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 f703e131424c04e5b16b4ce66e4b18fbf341b42e..a77727203d14cac45b3cb9918feaffb68df791d8 100644 --- a/src/server/app/View/user/signUp.php +++ b/src/server/app/View/user/signUp.php @@ -1,27 +1,25 @@ -<div class="container container-register"> +<main class="container-register"> <h2>Sign Up</h2> - <?php if (isset($model['error'])) {?> + <?php if (isset($model['error'])) { ?> <div class="alert-error alert-error-signup"> <p> - <?=$model['error']?> + <?= $model['error'] ?> </p> </div> - <?php }?> + <?php } ?> <div class="div-form-signup"> <form class="form-default" method="post" action='/signup'> <div class="form-input-default"> - <label for='id'>Id</label> - <input type="text" name='id' id='id' placeholder="Enter your id" class="input-default" value="<?=$_POST['id'] ?? ''?>"/> + <label for="email">Email</label> + <input type="email" name="email" id="email" placeholder="Enter your email" class="input-default" + value="<?= $_POST["email"] ?? "" ?>" /> </div> <div class="form-input-default"> - <label for='name'>Name</label> - <input type="text" name='name' id='name' placeholder="Enter your name" class="input-default" value="<?=$_POST['name'] ?? ''?>"/> - </div> - <div class="form-input-default"> - <label for='password'>Password</label> - <input type="password" name='password' id='password' placeholder="Enter your password" class="input-default"/> + <label for="password">Password</label> + <input type="password" name="password" id="password" placeholder="Enter your password" + class="input-default" /> </div> <button type="submit" class="btn-primary">Sign Up</button> </form> </div> -</div> +</main> \ No newline at end of file diff --git a/src/server/app/View/watchlist/create.php b/src/server/app/View/watchlist/create.php index 452da7e770a98b6c1b359cf54106ae74b8141776..f59205f20d114dceb4e8afffc2f9d97b021b9440 100644 --- a/src/server/app/View/watchlist/create.php +++ b/src/server/app/View/watchlist/create.php @@ -1,41 +1,72 @@ -<div class="container__default"> +<?php +function visibility() +{ + $id = 'visibility'; + $content = [ + "Public", + "Private" + ]; + $selected = 'Public'; + require __DIR__ . '/../components/select.php'; +} +?> + +<?php +function addItem() +{ + $triggerClasses = "btn-outline btn__add-item"; + $triggerText = 'Add Item'; + $triggerIcon = 'plus'; + $title = 'Search'; + $content = 'watchlistAddItem'; + require __DIR__ . '/../components/modal.php'; +} +?> + +<main> <h2 class="title-h2">New Watchlist</h2> - <div class="container-form-watchlist-create"> - <div class="subcon-form-watchlist-create"> - <form class="form-default form-watchlist-create"> + <div class="container__create-watchlist"> + <div class="container__form"> + <form class="form-default form__create-watchlist"> <div class="form-input-default"> - <label for="title">Title</label> - <input type="text" name="title" id="title" class="input-default" /> + <label for="title" class="input-required">Title</label> + <input type="text" name="title" id="title" class="input-default" + placeholder="Best Incest Anime and Drama" /> </div> <div class="form-input-default"> <label for="description">Description</label> - <textarea name="description" id="description" class="input-default"> </textarea> + <textarea name="description" id="description" class="input-default" + placeholder="Enter your watchlist description"></textarea> </div> <div class="form-input-default"> - <span>Visibility</span> - <div class="form-input-radio-default"> - <input type="radio" id="private" name="visibility" class="input-radio-default" value="private" /> - <label for="private">Private</label> - </div> - <div class="form-input-radio-default"> - <input type="radio" id="public" name="visibility" class="input-radio-default" value="public" /> - <label for="public">Public</label> - </div> + <<<<<<< HEAD <span>Visibility</span> + <div class="form-input-radio-default"> + <input type="radio" id="private" name="visibility" class="input-radio-default" + value="private" /> + <label for="private">Private</label> + </div> + <div class="form-input-radio-default"> + <input type="radio" id="public" name="visibility" class="input-radio-default" + value="public" /> + <label for="public">Public</label> + </div> + ======= + <label for="visibility" class="input-required">Visibility</label> + <?php visibility(); ?> + >>>>>>> 5e481bf333f2313cc24da65d28a6ace353cc8b3a </div> </form> - <div class="container-watchlist-items"> + <div class="watchlist-items"> <h3 class="title-h3">Items</h3> </div> + </div> - <div class="form-watchlist-actions"> - <button class="btn-outline btn-watchlist-add-item"> - <?php require PUBLIC_PATH . '/assets/icons/plus.php' ?> - Add Items - </button> - <label class="btn-primary-dark btn-watchlist-save"> + <div class="actions"> + <?php addItem(); ?> + <label class="btn btn-bold btn__save"> Save </label> </div> </div> -</div> \ No newline at end of file +</main> \ No newline at end of file diff --git a/src/server/app/View/watchlist/detail.php b/src/server/app/View/watchlist/detail.php index cb611db3b330d8b6aa5a3393a8f8dbe716d7e395..88a951bfe6adf568a8490f48c2384822cf719b8a 100644 --- a/src/server/app/View/watchlist/detail.php +++ b/src/server/app/View/watchlist/detail.php @@ -25,7 +25,7 @@ function pagination($currentPage, $totalPage) } ?> -<main class="container__default"> +<main> <article class="header"> <div class="detail"> <h2> @@ -61,7 +61,7 @@ function pagination($currentPage, $totalPage) </div> </article> <article id="catalogs" class="content"> - <?php foreach ($model['data']['catalogs']['items'] ?? [] as $catalog) : ?> + <?php foreach ($model['data']['catalogs']['items'] ?? [] as $catalog): ?> <?php catalogCard($catalog); ?> <?php endforeach; ?> <?php pagination($model['data']['catalogs']['currentPage'], $model['data']['catalogs']['totalPage']); ?> @@ -74,7 +74,7 @@ function pagination($currentPage, $totalPage) Submit </button> </form> - <?php foreach ($model['data']['comments']['items'] ?? [] as $comment) : ?> + <?php foreach ($model['data']['comments']['items'] ?? [] as $comment): ?> <?php commentCard($comment); ?> <?php endforeach; ?> <button id="show-more" class="btn-text"> diff --git a/src/server/config/bootstrap.php b/src/server/config/bootstrap.php index 184b0cefce6ab1d3741041d886c52daf1c8e777e..0639fbabe591cf61358741d4fe88b89a0beafeca 100644 --- a/src/server/config/bootstrap.php +++ b/src/server/config/bootstrap.php @@ -4,3 +4,14 @@ require_once __DIR__ . '/../app/Utils/EnvLoader.php'; // Load env (new EnvLoader(__DIR__ . '/../.env'))->load(); + +if (!function_exists('validateQueryParams')) { + function validateQueryParams($id, $content): ?string + { + if (!isset($content)) return null; + if (isset($_GET[$id]) && in_array($_GET[$id], $content, TRUE)) { + return $_GET[$id]; + } + return null; + } +} diff --git a/src/server/routes/view.php b/src/server/routes/view.php index 2b07af3964f72d562c579386e3497c9fade9866c..b4cf26aa6e889807b14394c16a3512cddde04850 100644 --- a/src/server/routes/view.php +++ b/src/server/routes/view.php @@ -6,6 +6,8 @@ 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/Middleware/UserAuthMiddleware.php'; + // Register routes // Home controllers @@ -29,6 +31,7 @@ Router::add('GET', '/catalog/([A-Za-z0-9]*)', CatalogController::class, 'detail' // Watchlist controllers Router::add('GET', '/watchlist/create', WatchlistController::class, 'create', []); Router::add('GET', '/watchlist/([A-Za-z0-9]*)', WatchlistController::class, 'detail', []); +Router::add("POST", "/cc/watchlist-item", WatchlistController::class, 'watchlistItem', []); // Execute Router::run();