diff --git a/src/Controllers/DormController.php b/src/Controllers/DormController.php index 983ab8621636fdf04808fb4c4b0481c74ab185b6..0d56dcbb5847ae239b61f84f31dfce145afe89bf 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\Models\Reviews; use app\Utils\FileManager; class DormController extends Controller @@ -27,6 +28,27 @@ class DormController extends Controller } $this->render('index', $data); } + + public function view($params) + { + $dormId = $params["dormId"]; + $dorm = Dorm::findById($dormId); + $medias = Media::findByDormId($dormId); + $owner = Owners::findById($dorm->owner_id); + $reviews = Reviews::findByDormId($dorm->dorm_id); + + if (Request::getMethod() === "POST") { + Response::redirect("/dorms/{$dorm->dorm_id}/edit"); + return; + } + + $this->render("dorm/view", [ + "dorm" => $dorm, + "medias" => $medias, + "owner" => $owner, + "reviews" => $reviews + ]); + } public function create() diff --git a/src/Controllers/ReviewController.php b/src/Controllers/ReviewController.php new file mode 100644 index 0000000000000000000000000000000000000000..dde38bbe933b399f356a9bf00f8b0eeff7023bff --- /dev/null +++ b/src/Controllers/ReviewController.php @@ -0,0 +1,127 @@ +<?php + + +namespace app\Controllers; + +use app\Core\Request; +use app\Core\Response; +use app\Forms\Field; +use app\Forms\Phone; +use app\Forms\Email; +use app\Forms\Required; +use app\Forms\Validation; +use app\Forms\Minimum; +use app\Forms\Number; +use app\Forms\Rating; +use app\Models\Owners; +use app\Models\Reviews; +use app\Models\User; +use app\Utils\Toast; + + +class ReviewController extends Controller +{ + public function __construct() + { + Response::$title = "Review | MyKos"; + } + + public function getAllByDormId($params) + { + $dorm = $params['dormId']; + $reviews = Reviews::findByDormId($dorm->dorm_id); + $this->render("reviews/edit", [ + "dorm_id" => $dorm->dorm_id, + "reviews" => $reviews + ]); + } + + public function create($params) + { + $form = $this->getReviewForm(); + $dormId = $params['dormId']; + + + if (Request::getMethod() === "POST") { + if ($form->validate(Request::getBody())) { + $review = Reviews::toModel($form->data); + $review->user_id = Request::getUser()->user_id; + $review->dorm_id = $dormId; + $review->save(); + Response::redirect("/dorms/{$dormId}"); + return; + } + var_dump($form->getFields()["errors"]); + exit(); + } + $userId = Request::getUser()->user_id; + $this->render("review/create", [ + "userId" => $userId, + ...$form->getFields() + ]); + } + + public function getReviewForm(){ + return new Validation([ + [new Field("rate", "Rating"), [new Required(), new Rating()]], + [new Field("description", "Deskripsi"), [new Required()]], + ]); + } + + private function setReviewFormData(Validation $form, Reviews $review) + { + $form->data["rate"] = $review->rate; + $form->data["description"] = $review->description; + } + + public function edit($params) + { + $reviewId = $params["reviewId"]; + $review = Reviews::findById($reviewId); + $userId = $review->user_id; + $dormId = $review->dorm_id; + $formReview = $this->getReviewForm(); + + if (Request::getMethod() === "POST") { + if ($formReview->validate(Request::getBody())) { + $review = Reviews::toModel($formReview->data); + $review->review_id = $reviewId; + $review->user_id = $userId; + $review->dorm_id = $dormId; + $review->save(); + Toast::success("Review berhasil diubah", true); + Response::redirect("/dorms/{$dormId}"); + return; + } + } else { + if (Request::getUser()->user_id !== $userId) { + Response::redirect("/dorms/{$dormId}"); + return; + } + $this->setReviewFormData($formReview, $review); + } + + $user = Request::getUser(); + $this->render("review/create", [ + "reviewId" => $reviewId, + ...$formReview->getFields() + ]); + } + + public function delete($params) + { + $dormId = $params["dormId"]; + + $form = new Validation([ + [new Field('review_id'), [new Number()]] + ]); + + if ($form->validate(Request::getBody())) { + $reviewId = $form->data["review_id"]; + Reviews::deleteById($reviewId); + } + + Toast::success("Review berhasil dihapus", true); + Response::redirect("/dorms/{$dormId}"); + } +} diff --git a/src/Core/Application.php b/src/Core/Application.php index 47cf365d2cfdcff700722ee31e4903b6d57828a9..1aea8de50f3bf686b3c532c48e78a25e6b271108 100644 --- a/src/Core/Application.php +++ b/src/Core/Application.php @@ -6,6 +6,7 @@ use app\Controllers\OwnerController; use app\Controllers\AuthController; use app\Controllers\DormController; use app\Controllers\UserController; +use app\Controllers\ReviewController; use app\Middlewares\AdminOnly; use app\Middlewares\AuthRequired; use app\Models\BaseModel; @@ -45,10 +46,16 @@ 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->methods(["GET", "POST"], "/dorms/{dormId}", [AuthRequired::class], DormController::class, 'view'); $this->router->methods(["GET", "POST"], "/dorms/{dormId}/media", [AdminOnly::class], DormController::class, 'media'); $this->router->methods(["GET", "POST"], "/dorms/{dormId}/edit", [AdminOnly::class], DormController::class, 'edit'); + $this->router->methods(["GET", "POST"], "/dorms/{dormId}/reviews", [AuthRequired::class], ReviewController::class, 'getAllByDormId'); + $this->router->methods(["GET", "POST"], "/dorms/{dormId}/add-review", [AuthRequired::class], ReviewController::class, 'create'); $this->router->methods(["GET", "POST"], "/account/edit", [AuthRequired::class], UserController::class, 'edit'); $this->router->methods(["GET", "POST"], "/account/editPassword", [AuthRequired::class], UserController::class, 'editPassword'); + + $this->router->methods(["GET", "POST"], "/reviews/{reviewId}", [AuthRequired::class], ReviewController::class, 'edit'); + $this->router->delete("/dorms/{dormId}", [AuthRequired::class], ReviewController::class, 'delete'); } } diff --git a/src/Forms/Rating.php b/src/Forms/Rating.php new file mode 100644 index 0000000000000000000000000000000000000000..9167ca1ff04bb105098b6f545d9ab3f7836d9a64 --- /dev/null +++ b/src/Forms/Rating.php @@ -0,0 +1,21 @@ +<?php + +namespace app\Forms; + + +class Rating extends Rule +{ + public function validate(string $label, &$value): ?string + { + if (is_int($value) && 1 <= $value && $value <= 5) { + return null; + } + + try { + $value = intval($value); + return null; + } catch (\Throwable $th) { + return "$label harus berupa angka 1 sampai 5"; + } + } +} \ No newline at end of file diff --git a/src/Models/Reviews.php b/src/Models/Reviews.php new file mode 100644 index 0000000000000000000000000000000000000000..f5fe8a6911fc5a71031182dd56bf4d1f8b246f06 --- /dev/null +++ b/src/Models/Reviews.php @@ -0,0 +1,22 @@ +<?php + +namespace app\Models; + +class Reviews extends BaseModel +{ + public int $review_id; + public int $user_id; + public int $dorm_id; + public int $kos_id; + public int $rate; + public ?string $description; + public string $created_at; + protected static string $table = 'reviews'; + protected static string $primaryKey = 'review_id'; + + public static function findByDormId(int $dormId) + { + return self::where(["dorm_id" => $dormId]); + } + +} diff --git a/src/Views/dorm/view.php b/src/Views/dorm/view.php new file mode 100644 index 0000000000000000000000000000000000000000..b802a5cbbf44e02b4f39abaa258827166c964009 --- /dev/null +++ b/src/Views/dorm/view.php @@ -0,0 +1,130 @@ +@extends('layouts/base') +@@head +<link rel="stylesheet" href="/static/styles/dorm-view.css?v=<?php + +use app\Models\User; + + echo time(); ?>"> +@@endhead +<div class="container"> + <? 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; ?> + + <div class="info"> + <div class="info-dorm"> + <h1 class="title"><?= $dorm->name ?></h1> + <h2 class="address"><?= $dorm->address ?></h2> + <h3 class="city"><?= $dorm->city ?></h3> + <h3 class="description"><?= $dorm->description ?></h3> + </div> + + + <div class="info-booking"> + + <p class="price">Rp<?= number_format($dorm->price, 0, ',', '.') ?> + <span> / bulan</span> + </p> + + <p class="info-owner">Info Pemilik</p> + <p><span class="person-icon">👤</span>: <?= $owner->name ?></p> + <p><span class="phone-icon"> ☎</span> : <?= $owner->phone_number ?></p> + </div> + </div> + + <? if ($user->is_admin ?? false) : ?> + <div class="edit-admin"> + <a href="/dorms/<?= $dorm->dorm_id ?>/edit" class="btn btn-primary btn-edit-kos">Edit Kos</a> + </div> + <? endif; ?> + + <div class="reviews"> + <h1> Review Kos Ini</h1> + <? if (!$user->is_admin ?? false) : ?> + <div class="add-review"> + <a href="/dorms/<?= $dorm->dorm_id ?>/add-review" class="btn btn-primary btn-add-review"> + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" + width="12" height="12" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> + </svg> + Tambahkan Review + </a> + </div> + <? endif; ?> + + <ul class="review-list"> + <? foreach ($reviews as $review) : ?> + <? $reviewer = User::findById($review->user_id); ?> + <li class="review-item"> + <div class="review-item-content"> + <h3 class="review-item-name"><?= $reviewer->name; ?></h3> + <p class="review-item-rate"> + <? + $rating = $review->rate; + $stars = str_repeat("★", $rating); + $emptyStars = str_repeat("☆", 5 - $rating); + echo $stars . $emptyStars; + ?> + </p> + <p class="review-item-description"><?= $review->description ?></p> + </div> + <div class="review-actions"> + <? if (($user->is_admin ?? false) || ($user->user_id == $reviewer->user_id ?? false)) : ?> + <? if ($user->user_id == $reviewer->user_id ?? false) : ?> + <a href="/reviews/<?= $review->review_id ?>" class="btn btn-outlined edit-review" data-dialog="add-owner-dialog"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" + stroke-width="2" stroke="currentColor" width="20" height="20"> + <path stroke-linecap="round" stroke-linejoin="round" + d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" /> + </svg> + </a> + <? endif ?> + <form method="POST"> + <button type="button" class="btn btn-danger dialog-btn action-btn" + data-dialog="delete-<?= $review->review_id ?>"><svg xmlns="http://www.w3.org/2000/svg" fill="none" + viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" width="20" height="20"> + <path stroke-linecap="round" stroke-linejoin="round" + d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" /> + </svg> + </button> + <input type="hidden" name="review_id" value="<?= $review->review_id ?>" /> + <input type="hidden" name="_method" value="DELETE" /> + <div class="dialog-wrapper delete-<?= $review->review_id ?>"> + <div class="dialog-content"> + <h4 class="confirm-title">Apakah anda yakin ingin menghapus review?</h4> + <div class="confirm-action"> + <button type="button" class="btn btn-outlined dialog-btn" + data-dialog="delete-<?= $review->review_id ?>">Batal</button> + <button type="submit" class="btn btn-danger">Hapus</button> + </div> + </div> + </div> + </form> + <? endif; ?> + </div> + </li> + <? endforeach; ?> + </ul> + + </div> +</div> \ No newline at end of file diff --git a/src/Views/review/create.php b/src/Views/review/create.php new file mode 100644 index 0000000000000000000000000000000000000000..8eeee8e48c45664b6c86a42ac587699e336efe6a --- /dev/null +++ b/src/Views/review/create.php @@ -0,0 +1,31 @@ +@extends('layouts/base') +@@head +<link rel="stylesheet" href="/static/styles/create-dorm.css"> +@@endhead +<div> + <h1><?= isset($reviewId) ? "Edit Review" : "Tambahkan Review" ?></h1> + <form method="POST" novalidate class="create-form"> + <div class="input-group"> + <label for="rate" class="required">Rating</label> + <select autofocus name="rate"> + <?php + $ratings = [1, 2, 3, 4, 5]; + foreach ($ratings as $rating) : + $star = str_repeat("☆", $rating);?> + <option value=<?=$rating?> <?if ($fields["rate"] === $rating) : ?> selected <? endif; ?>><?= $star?></option> + <? endforeach; ?> + </select> + </div> + <div class="input-group"> + <label for="description" class="required">Deskripsi</label> + <textarea name="description" placeholder="Deskripsi"><?= $fields["description"] ?? '' ?></textarea> + <?php + if (isset($errors) && array_key_exists('description', $errors)) { + echo '<div class="error">' . $errors["description"] . '</div>'; + } + ?> + </div> + <button class="btn btn-primary"> + <?= isset($reviewId) ? "Edit" : "Tambah" ?> Review + </form> +</div> \ No newline at end of file diff --git a/src/public/static/styles/dorm-view.css b/src/public/static/styles/dorm-view.css new file mode 100644 index 0000000000000000000000000000000000000000..c6244cee65f70aaf8495342a3486b52eec789671 --- /dev/null +++ b/src/public/static/styles/dorm-view.css @@ -0,0 +1,134 @@ +.container { + max-width: 1080px; + width: 100%; + margin: 0 auto; +} +.slides-container { + aspect-ratio: 16 / 9; + } + +.info { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: flex-start; +} + +.info-dorm { + flex-basis: calc(65% - 20px); +} + +.title { + font-size: 24px; + margin-bottom: 10px; +} + +.address { + font-size: 18px; + margin-bottom: 5px; +} + +.city { + font-size: 16px; + margin-bottom: 10px; +} + +.description { + font-size: 16px; + line-height: 1.4; + margin-bottom: 10px; +} + +.info-booking { + flex-basis: calc(35% - 20px); + background-color: #f5f5f5; + padding: 20px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + transition: background-color 0.3s, box-shadow 0.3s; + position: sticky; + top: 100px; +} + +.main-content { + margin-bottom: 100px; +} + +.price { + font-size: 24px; + font-weight: bold; + margin-bottom: 10px; + color: var(--color-primary); +} + +.info-owner{ + font-weight: bold; +} + +.person-icon { + display: inline-block; + margin-right: 5px; + font-size: 20px; + color: var(--color-primary); +} +.phone-icon { + display: inline-block; + margin-right: 5px; + font-size: 20px; + color: var(--color-primary); +} + +.add-review, +.edit-admin { + margin-top: 20px; + margin-bottom: 20px; +} + +.btn-edit-kos, +.btn-add-review { + display: inline-block; +} + +ul.review-list { + list-style: none; + padding: 0; +} + +li.review-item { + margin-top: 20px; + margin-bottom: 20px; + border: 1px solid #ccc; + padding: 10px; + background-color: #f9f9f9; + display: flex; + justify-content: space-between; + align-items: center; +} + +div.review-item-content { + display: flex; + flex-direction: column; +} + +h3.review-item-name { + margin: 0; + font-weight: bold; +} + +p.review-item-rate { + margin: 5px 0; + font-weight: bold; + font-size: 16px; + color: #ff9900; +} + +p.review-item-description { + margin: 5px 0; + font-size: 14px; + color: #333; +} + +.review-actions { + display: flex; + gap: 10px; +}