diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a293e6d4667525858c8507c65d858f8502eb642e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +src/public/media/ \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4a17bdf2f114517c3d9af6d65de0f620729c5743..e4b068f7d6b0abea5c55ddaffa3e03f3346ccf41 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,7 @@ services: - 8080:80 volumes: - ./src:/var/www/html + - ./src/php.ini:/usr/local/etc/php/php.ini networks: - php-network depends_on: diff --git a/src/Controllers/DormController.php b/src/Controllers/DormController.php index 24123c6e17b2cb42acc6524e3b4a66f7b7faa705..6c1f169b5d428e4d89fbd7df6aa773c53762d473 100644 --- a/src/Controllers/DormController.php +++ b/src/Controllers/DormController.php @@ -11,6 +11,7 @@ use app\Forms\Validation; use app\Models\Dorm; use app\Models\Media; use app\Models\Owners; +use app\Utils\FileManager; class DormController extends Controller { @@ -48,6 +49,14 @@ class DormController extends Controller public function media($params) { $dormId = $params["dormId"]; + if (Request::getMethod() === "POST") { + $files = Request::getFiles("medias"); + if ($files !== null) { + $this->uploadMedia($files, $dormId); + Response::redirect("/dorms/{$dormId}/media"); + return; + } + } $dorm = Dorm::findById($dormId); $medias = Media::findByDormId($dormId); @@ -56,4 +65,21 @@ class DormController extends Controller "medias" => $medias ]); } + + private function uploadMedia($files, $dorm_id) + { + foreach ($files["name"] as $index => $name) { + $filePath = FileManager::getPathFor($name, $files["type"][$index]); + if ($filePath == false) { + continue; + } + $media = new Media(); + $media->dorm_id = $dorm_id; + $media->alt_text = $name; + $media->type = explode("/", $files["type"][$index])[0] === "image" ? "photo" : "video"; + $media->endpoint = $filePath; + $media->save(); + FileManager::uploadToPublic($files["tmp_name"][$index], $filePath); + } + } } diff --git a/src/Core/Application.php b/src/Core/Application.php index 4f3757b49d1536232ec2c021b49579ef4ae2af2f..563a07596c957af78497880f89d73a4418704a7e 100644 --- a/src/Core/Application.php +++ b/src/Core/Application.php @@ -44,6 +44,6 @@ class Application $this->router->get("/me", [AuthRequired::class], AuthController::class, 'me'); $this->router->methods(["GET", "POST"], "/dorms/create", [AdminOnly::class], DormController::class, 'create'); - $this->router->get("/dorms/{dormId}/media", [AdminOnly::class], DormController::class, 'media'); + $this->router->methods(["GET", "POST"], "/dorms/{dormId}/media", [AdminOnly::class], DormController::class, 'media'); } -} +} \ No newline at end of file diff --git a/src/Core/Request.php b/src/Core/Request.php index 093acacd945994b7554b0d586440645843d6cbf6..d5798187ea5a3679f5a994716ccb618bae9d35f7 100644 --- a/src/Core/Request.php +++ b/src/Core/Request.php @@ -54,4 +54,9 @@ class Request { return isset($_SESSION['user']); } + + public static function getFiles($fieldName) + { + return $_FILES[$fieldName] ?? null; + } } diff --git a/src/Core/Router.php b/src/Core/Router.php index 6fb90f3126cb6623893bc9f43f348670787abb4a..6ed3306ed2b023a0b456680a1c11f61a97c39d84 100644 --- a/src/Core/Router.php +++ b/src/Core/Router.php @@ -138,6 +138,9 @@ class Router $uri = Request::getPath(); foreach ($this->dynamicRoutes as $routeRegex => $routeHandler) { if (preg_match($routeRegex, $uri, $matches)) { + if (array_key_exists(Request::getMethod(), $routeHandler) === false) { + return false; + } [$controllerClass, $middlewares, $action, $vars] = $routeHandler[Request::getMethod()]; $controller = new $controllerClass(); $params = []; diff --git a/src/Utils/FileManager.php b/src/Utils/FileManager.php new file mode 100644 index 0000000000000000000000000000000000000000..07f5467fd4d60b126e4ae413c3e08acb0af355c4 --- /dev/null +++ b/src/Utils/FileManager.php @@ -0,0 +1,29 @@ +<?php + +namespace app\Utils; + +class FileManager +{ + public static function getPathFor($fileName, $fileType) + { + // add timestamp to file name + $fileName = time() . $fileName; + if (str_starts_with($fileType, "image")) { + return "/media/images/$fileName"; + } else if (str_starts_with($fileType, "video")) { + return "/media/videos/$fileName"; + } else { + return false; + } + } + + public static function uploadToPublic($tmp_path, $filePath) + { + $path = __DIR__ . "/../public" . $filePath; + $pathInfo = pathinfo($path); + if (is_dir($pathInfo["dirname"]) === false) { + mkdir($pathInfo["dirname"], 0755, true); + } + move_uploaded_file($tmp_path, $path); + } +} diff --git a/src/Views/dorm/media.php b/src/Views/dorm/media.php index a57efda74ab6a2a9f01ee159cc8c0b7f3675f729..da82b06eb91e7d1cfc0ae211f8d85db41c296c36 100644 --- a/src/Views/dorm/media.php +++ b/src/Views/dorm/media.php @@ -1 +1,46 @@ -@extends('layouts/base') \ No newline at end of file +@extends('layouts/base') +@@head +<link rel="stylesheet" href="/static/styles/dorm-media.css"> +@@endhead +<div class="container"> + <h1 class="">Media "<?= $dorm->name ?>"</h1> + <? if (count($medias) > 0) : ?> + <section class="slider-wrapper"> + <button class="slide-arrow slide-arrow-prev"> + ‹ + </button> + + <button class="slide-arrow slide-arrow-next"> + › + </button> + + + <ul class="slides-container"> + <? foreach ($medias as $media) : ?> + <li class="slide"> + <? if ($media->type == "video") : ?> + <video src="<?= $media->endpoint ?>" alt="<?= $media->alt_text ?>" controls></video> + <? else : ?> + <img src="<?= $media->endpoint ?>" alt="<?= $media->alt_text ?>"> + <? endif; ?> + </li> + <?php endforeach; ?> + </ul> + </section> + <? endif; ?> + <form method="post" enctype="multipart/form-data"> + <input type="file" name="medias[]" multiple accept="image/*, video/*" /> + <button type="button" class="btn btn-primary dialog-btn" data-dialog="upload-media-dialog">Upload</button> + <div class="dialog-wrapper upload-media-dialog"> + <div class="dialog-content"> + <h4 class="confirm-title"> + Apakah anda yakin ingin upload media? + </h4> + <div class="confirm-action"> + <button type="button" class="btn btn-outlined dialog-btn" data-dialog="upload-media-dialog">Batal</button> + <button type="submit" class="btn btn-primary">Upload</button> + </div> + </div> + </div> + </form> +</div> \ No newline at end of file diff --git a/src/php.ini b/src/php.ini new file mode 100644 index 0000000000000000000000000000000000000000..f25c73f68ba215a59ad416e6459a0c2074069675 --- /dev/null +++ b/src/php.ini @@ -0,0 +1,4 @@ +file_uploads = On +upload_max_filesize = 100M + +post_max_size = 100M \ No newline at end of file diff --git a/src/public/static/scripts/index.js b/src/public/static/scripts/index.js index 9616e1db28f597f804170c248f466868ab07ff96..5b1f26d67a2024bfff0c0050c08b4d1a6b48a7ae 100644 --- a/src/public/static/scripts/index.js +++ b/src/public/static/scripts/index.js @@ -45,3 +45,19 @@ window.addEventListener("load", () => { }, 5000); }); }); + +const slidesWrappers = document.querySelectorAll(".slider-wrapper"); +slidesWrappers.forEach((slideWrapper) => { + const slidesContainer = slideWrapper.querySelector(".slides-container"); + const slide = slidesContainer.querySelector(".slide"); + const prevButton = slideWrapper.querySelector(".slide-arrow-prev"); + const nextButton = slideWrapper.querySelector(".slide-arrow-next"); + nextButton.addEventListener("click", (event) => { + const slideWidth = slide.clientWidth; + slidesContainer.scrollLeft += slideWidth; + }); + prevButton.addEventListener("click", () => { + const slideWidth = slide.clientWidth; + slidesContainer.scrollLeft -= slideWidth; + }); +}); diff --git a/src/public/static/styles/dorm-media.css b/src/public/static/styles/dorm-media.css new file mode 100644 index 0000000000000000000000000000000000000000..d94b583a0081f54a71d982875be419b0defa6238 --- /dev/null +++ b/src/public/static/styles/dorm-media.css @@ -0,0 +1,5 @@ +.container { + max-width: 720px; + width: 100%; + margin: 0 auto; +} diff --git a/src/public/static/styles/main.css b/src/public/static/styles/main.css index cb9a0a41c8d8e905cef448f4dc6ac7d84319fb92..7995ecda11e0819e0dd46e95088345a14ff07699 100644 --- a/src/public/static/styles/main.css +++ b/src/public/static/styles/main.css @@ -51,6 +51,7 @@ h1 { .navbar_main { position: fixed; top: 0; + z-index: 40; inset-inline: 0; background: white; border-bottom: 1px solid var(--color-gray); @@ -265,6 +266,7 @@ td { .dialog-wrapper { position: fixed; + z-index: 50; inset: 0; background-color: rgba(0, 0, 0, 0.5); display: none; @@ -279,7 +281,7 @@ td { .dialog-content { animation: 0.1s ease-out 0s 1 zoomIn; background-color: white; - padding: 1rem; + padding: 2rem; border-radius: 0.25rem; width: 90%; max-width: 380px; @@ -300,7 +302,7 @@ td { .dialog-content .confirm-title { font-size: var(--text-lg); font-weight: 700; - margin-bottom: 1rem; + margin-bottom: 2rem; } .dialog-content .confirm-action { @@ -314,6 +316,10 @@ td { flex: 1; } +.dialog-content .confirm-action .btn { + padding-block: 0.5rem; +} + .toast { position: fixed; z-index: 50; @@ -356,7 +362,7 @@ td { width: 100%; bottom: 0; height: 0.25rem; - transition: all 5s linear; + transition: width 5s linear; } .toast.loaded .toast-progress { @@ -370,3 +376,73 @@ td { .toast.toast-success .toast-progress { background-color: var(--color-primary); } + +.slider-wrapper { + margin: 1rem; + position: relative; + overflow: hidden; +} +.slides-container { + max-width: 100%; + width: 100%; + aspect-ratio: 16 / 9; + display: flex; + list-style: none; + margin: 0; + padding: 0; + overflow: scroll; + scroll-behavior: smooth; +} +.slide { + width: 100%; + height: 100%; + flex: 1 0 100%; +} + +.slide img, +video { + width: 100%; + height: 100%; + object-fit: contain; +} + +.slides-container { + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* Internet Explorer 10+ */ +} +/* WebKit */ +.slides-container::-webkit-scrollbar { + width: 0; + height: 0; +} +.slide-arrow { + position: absolute; + display: flex; + top: 0; + bottom: 0; + margin: auto; + height: 4rem; + background-color: white; + border: none; + width: 2rem; + font-size: 3rem; + padding: 0; + cursor: pointer; + opacity: 0.5; + transition: opacity 100ms; + z-index: 10; +} +.slide-arrow:hover, +.slide-arrow:focus { + opacity: 1; +} +.slide-arrow-prev { + left: 0; + padding-left: 0.25rem; + border-radius: 0 2rem 2rem 0; +} +.slide-arrow-next { + right: 0; + padding-left: 0.75rem; + border-radius: 2rem 0 0 2rem; +}