From cf3a43f1a7499a5f99a673c0593ffe3e386ec488 Mon Sep 17 00:00:00 2001
From: arsaizdihar <arsadihar@gmail.com>
Date: Tue, 3 Oct 2023 16:43:44 +0700
Subject: [PATCH] feat: list kost

---
 src/Controllers/AuthController.php |  4 ---
 src/Controllers/DormController.php | 10 ++++++
 src/Core/Application.php           |  4 +--
 src/Core/Request.php               | 10 ++++++
 src/Models/BaseModel.php           | 23 ++++++-------
 src/Models/Dorm.php                | 53 ++++++++++++++++++++++++++++++
 src/Views/index.php                | 27 +++++++++++++--
 src/public/static/styles/home.css  | 49 +++++++++++++++++++++++++++
 src/public/static/styles/main.css  | 11 +++++--
 9 files changed, 169 insertions(+), 22 deletions(-)

diff --git a/src/Controllers/AuthController.php b/src/Controllers/AuthController.php
index 235a327..97a5bb1 100644
--- a/src/Controllers/AuthController.php
+++ b/src/Controllers/AuthController.php
@@ -17,10 +17,6 @@ use app\Utils\Toast;
 class AuthController extends Controller
 {
 
-  public function index()
-  {
-    $this->render('index');
-  }
 
   public function login()
   {
diff --git a/src/Controllers/DormController.php b/src/Controllers/DormController.php
index 6c1f169..d450921 100644
--- a/src/Controllers/DormController.php
+++ b/src/Controllers/DormController.php
@@ -15,6 +15,16 @@ use app\Utils\FileManager;
 
 class DormController extends Controller
 {
+
+  public function list()
+  {
+    $page = Request::getPage();
+    $data = Dorm::getAllFiltered($page);
+    $data["page"] = $page;
+    $this->render('index', $data);
+  }
+
+
   public function create()
   {
     $form = new Validation([
diff --git a/src/Core/Application.php b/src/Core/Application.php
index 563a075..c24a4eb 100644
--- a/src/Core/Application.php
+++ b/src/Core/Application.php
@@ -32,7 +32,7 @@ class Application
   public function configureRoutes()
   {
     $this->router = new Router();
-    $this->router->get('/', [], AuthController::class, 'index');
+    $this->router->get('/', [], DormController::class, 'list');
     $this->router->methods(["GET", "POST"], '/login', [],  AuthController::class, 'login');
     $this->router->methods(["GET", "POST"], '/register', [],  AuthController::class, 'register');
     $this->router->get('/logout', [],  AuthController::class, 'logout');
@@ -46,4 +46,4 @@ class Application
     $this->router->methods(["GET", "POST"], "/dorms/create", [AdminOnly::class], DormController::class, 'create');
     $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 d579818..35f1662 100644
--- a/src/Core/Request.php
+++ b/src/Core/Request.php
@@ -59,4 +59,14 @@ class Request
   {
     return $_FILES[$fieldName] ?? null;
   }
+
+  public static function getPage()
+  {
+    $page = $_GET['page'] ?? "1";
+    try {
+      return intval($page);
+    } catch (\Throwable $th) {
+      return 1;
+    }
+  }
 }
diff --git a/src/Models/BaseModel.php b/src/Models/BaseModel.php
index 0e341c7..886477f 100644
--- a/src/Models/BaseModel.php
+++ b/src/Models/BaseModel.php
@@ -4,17 +4,18 @@ namespace app\Models;
 
 use app\Core\Application;
 use app\Core\Database;
+use PDO;
 
 #[\AllowDynamicProperties]
 class BaseModel
 {
-  protected static Database $db;
+  protected static PDO $db;
   protected static string $table = "";
   protected static string $primaryKey = "";
 
   public static function connect()
   {
-    self::$db = Application::$db;
+    self::$db = Application::$db->pdo;
   }
 
   public static function toModel($data): self
@@ -28,7 +29,7 @@ class BaseModel
     return $model;
   }
 
-  protected static function toModelArray($data)
+  public static function toModelArray($data)
   {
     $models = [];
     foreach ($data as $row) {
@@ -39,14 +40,14 @@ class BaseModel
 
   public static function all($orderBy = null)
   {
-    $stmt = self::$db->pdo->prepare("SELECT * FROM " . static::$table . ($orderBy ? " ORDER BY " . $orderBy : ""));
+    $stmt = self::$db->prepare("SELECT * FROM " . static::$table . ($orderBy ? " ORDER BY " . $orderBy : ""));
     $stmt->execute();
     return self::toModelArray($stmt->fetchAll());
   }
 
   public static function findById(int $id): static
   {
-    $stmt = self::$db->pdo->prepare("SELECT * FROM " . static::$table . " WHERE " . static::$primaryKey . " = :id");
+    $stmt = self::$db->prepare("SELECT * FROM " . static::$table . " WHERE " . static::$primaryKey . " = :id");
     $stmt->bindValue(":id", $id);
     $stmt->execute();
     $data = $stmt->fetch();
@@ -58,7 +59,7 @@ class BaseModel
 
   public static function deleteById(int $id)
   {
-    $stmt = self::$db->pdo->prepare("DELETE FROM " . static::$table . " WHERE " . static::$primaryKey . " = :id");
+    $stmt = self::$db->prepare("DELETE FROM " . static::$table . " WHERE " . static::$primaryKey . " = :id");
     $stmt->bindValue(":id", $id);
     $stmt->execute();
     return $stmt->rowCount();
@@ -73,7 +74,7 @@ class BaseModel
     }
     $where = substr($where, 0, -5);
 
-    $stmt = self::$db->pdo->prepare("SELECT * FROM " . static::$table . " WHERE " . $where);
+    $stmt = self::$db->prepare("SELECT * FROM " . static::$table . " WHERE " . $where);
     foreach ($arrFilter as $key => $value) {
       $stmt->bindValue(":" . $key, $value);
     }
@@ -90,7 +91,7 @@ class BaseModel
     }
     $where = substr($where, 0, -5);
 
-    $stmt = self::$db->pdo->prepare("SELECT * FROM " . static::$table . " WHERE " . $where . " LIMIT 1");
+    $stmt = self::$db->prepare("SELECT * FROM " . static::$table . " WHERE " . $where . " LIMIT 1");
     foreach ($arrFilter as $key => $value) {
       $stmt->bindValue(":" . $key, $value);
     }
@@ -122,7 +123,7 @@ class BaseModel
 
     $sql = "INSERT INTO " . static::$table . " (" . implode(",", $keys) . ") VALUES (:" . implode(",:", $keys) . ") RETURNING " . static::$primaryKey . ";";
 
-    $stmt = self::$db->pdo->prepare($sql);
+    $stmt = self::$db->prepare($sql);
 
     foreach ($fields as $key => $value) {
       $stmt->bindValue(":" . $key, $value);
@@ -149,7 +150,7 @@ class BaseModel
     $sql = substr($sql, 0, -2);
     $sql .= " WHERE " . static::$primaryKey . " = :" . static::$primaryKey . ";";
 
-    $stmt = self::$db->pdo->prepare($sql);
+    $stmt = self::$db->prepare($sql);
 
     foreach ($fields as $key => $value) {
       $stmt->bindValue(":" . $key, $value);
@@ -163,7 +164,7 @@ class BaseModel
   {
     $sql = "DELETE FROM " . static::$table . " WHERE " . static::$primaryKey . " = :" . static::$primaryKey . ";";
 
-    $stmt = self::$db->pdo->prepare($sql);
+    $stmt = self::$db->prepare($sql);
 
     $stmt->bindValue(":" . static::$primaryKey, $this->{static::$primaryKey});
 
diff --git a/src/Models/Dorm.php b/src/Models/Dorm.php
index 3157abf..89edd8d 100644
--- a/src/Models/Dorm.php
+++ b/src/Models/Dorm.php
@@ -14,4 +14,57 @@ class Dorm extends BaseModel
   public int $owner_id;
   protected static string $table = "dorms";
   protected static string $primaryKey = "dorm_id";
+
+  public static function getAllFiltered($page = 1, $orderBy = "dorm_id DESC", $search = "")
+  {
+    $limit = 4;
+    $offset = ($page - 1) * $limit;
+
+    $stmt = self::$db->prepare(
+      $search ?
+        "SELECT * FROM dorms WHERE name ILIKE :1 ORDER BY :2 LIMIT :3 OFFSET :4" :
+        "SELECT * FROM dorms ORDER BY :2 LIMIT :3 OFFSET :4"
+    );
+
+    if ($search) {
+      $stmt->bindValue(":1", "%{$search}%");
+    }
+    $stmt->bindValue(":2", $orderBy);
+    $stmt->bindValue(":3", $limit);
+    $stmt->bindValue(":4", $offset);
+
+    $stmt->execute();
+    $dorms = self::toModelArray($stmt->fetchAll());
+
+    $stmt = self::$db->prepare(
+      $search ?
+        "SELECT COUNT(dorm_id) FROM dorms WHERE name ILIKE :1" :
+        "SELECT COUNT(dorm_id) FROM dorms"
+    );
+
+    if ($search) {
+      $stmt->bindValue(":1", "%{$search}%");
+    }
+
+    $stmt->execute();
+
+    $count = $stmt->fetchColumn();
+    $totalPage = ceil($count / $limit);
+
+    $dormIds = array_map(fn ($dorm) => $dorm->dorm_id, $dorms);
+
+    $stmt = self::$db->prepare("SELECT * FROM medias
+      NATURAL JOIN (
+        SELECT MIN(media_id) AS media_id, dorm_id FROM medias WHERE type = 'photo' AND dorm_id IN (" . implode(",", $dormIds) . ") GROUP BY dorm_id
+        ) AS m");
+    $stmt->execute();
+
+    $medias = Media::toModelArray($stmt->fetchAll());
+    $medias = array_reduce($medias, function ($acc, $media) {
+      $acc[$media->dorm_id] = $media;
+      return $acc;
+    }, []);
+
+    return ["dorms" => $dorms, "medias" => $medias, "totalPage" => $totalPage];
+  }
 }
diff --git a/src/Views/index.php b/src/Views/index.php
index 4cf6a43..077aa69 100644
--- a/src/Views/index.php
+++ b/src/Views/index.php
@@ -7,8 +7,29 @@
   <p class="hero-desc">Dapatkan infonya dan langsung sewa di MyKos.
   </p>
   <? if ($user->is_admin ?? false) : ?>
-    <div class="cta-admin">
-      <a href="/dorms/create" class="btn btn-primary">Tambah Kos</a>
-    </div>
+  <div class="cta-admin">
+    <a href="/dorms/create" class="btn btn-primary">Tambah Kos</a>
+  </div>
   <? endif; ?>
+  <ul class="dorm_list">
+    <? foreach ($dorms as $dorm) : ?>
+    <li class="dorm_item">
+      <a href="/dorms/<?= $dorm->dorm_id ?>">
+        <? $media = array_key_exists($dorm->dorm_id, $medias) ? $medias[$dorm->dorm_id] : null ?>
+        <? if ($media) : ?>
+        <img class="dorm_image" src="<?= $media->endpoint ?>" alt="<?= $media->alt_text ?>" />
+        <? else : ?>
+        <div class="dorm_image"></div>
+        <? endif; ?>
+        <div class="dorm_item__content">
+          <h3 class="dorm_item__title line-clamp-1"><?= $dorm->name ?></h3>
+          <p class="dorm_item__city"><?= $dorm->city ?></p>
+          <p class="dorm_item__price">Rp<?= number_format($dorm->price, 0, ',', '.') ?>
+            <span> / bulan</span>
+          </p>
+        </div>
+      </a>
+    </li>
+    <? endforeach; ?>
+  </ul>
 </section>
\ No newline at end of file
diff --git a/src/public/static/styles/home.css b/src/public/static/styles/home.css
index 6d0bf26..d9305c5 100644
--- a/src/public/static/styles/home.css
+++ b/src/public/static/styles/home.css
@@ -21,3 +21,52 @@
   min-width: 10rem;
   text-align: center;
 }
+
+.dorm_list {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 2rem;
+  margin-top: 4rem;
+}
+
+.dorm_item__title {
+  color: #303030;
+  font-size: var(--text-sm);
+  margin-top: 0.5rem;
+}
+
+.dorm_item__city {
+  color: #404040;
+  font-size: var(--text-sm);
+  font-weight: 700;
+  margin-top: 0.5rem;
+}
+
+.dorm_item__price {
+  color: #303030;
+  font-size: var(--text-base);
+  font-weight: 700;
+  margin-top: 0.5rem;
+}
+
+.dorm_item__price span {
+  font-weight: 400;
+}
+
+.dorm_image {
+  width: 100%;
+  aspect-ratio: 4/3;
+  object-fit: cover;
+  background-color: white;
+  border-radius: 1rem;
+  border: 1px solid var(--color-gray-darker);
+  transition: all 0.1s ease-in-out;
+}
+
+.dorm_item:hover .dorm_image {
+  transform: scale(1.05);
+}
+
+div.dorm_image {
+  background-color: var(--color-gray-darker);
+}
diff --git a/src/public/static/styles/main.css b/src/public/static/styles/main.css
index 7995ecd..1d5b233 100644
--- a/src/public/static/styles/main.css
+++ b/src/public/static/styles/main.css
@@ -17,6 +17,13 @@ html {
   --text-sm: 0.875rem;
 }
 
+.line-clamp-1 {
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 1;
+}
+
 body {
   min-height: 100%;
   display: flex;
@@ -385,7 +392,7 @@ td {
 .slides-container {
   max-width: 100%;
   width: 100%;
-  aspect-ratio: 16 / 9;
+  aspect-ratio: 4 / 3;
   display: flex;
   list-style: none;
   margin: 0;
@@ -403,7 +410,7 @@ td {
 video {
   width: 100%;
   height: 100%;
-  object-fit: contain;
+  object-fit: cover;
 }
 
 .slides-container {
-- 
GitLab