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">
+        &#8249;
+      </button>
+
+      <button class="slide-arrow slide-arrow-next">
+        &#8250;
+      </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;
+}