diff --git a/public/styles/views/Shop.css b/public/styles/views/Shop.css index 5d4c2bb..bd9d5dd 100644 --- a/public/styles/views/Shop.css +++ b/public/styles/views/Shop.css @@ -14,7 +14,7 @@ box-shadow: rgba(0, 0, 0, 0.05) 0px 6px 24px 0px, rgba(0, 0, 0, 0.08) 0px 0px 0px 1px; } -#item-grid > a:focus{ +#item-grid > a:focus { background-color: transparent; } @@ -61,4 +61,52 @@ article header { #item-grid { grid-template-columns: repeat(1, 1fr); } +} + +.pagination { + display: flex; + list-style: none; + border-radius: 0.25rem; + gap: 0.45rem; + margin-top: 2cm; +} + + +.page-item { + --bs-padding-x: 0.5rem; + --bs-padding-y: 0.25rem; +} + +.page-link { + position: relative; + display: block; + padding: var(--bs-padding-y) var(--bs-padding-x); + text-decoration: none; + transition: color .25s ease-in-out, background-color .25s ease-in-out; + outline: 1px solid #dee2e6; +} + +.page-link:hover { + z-index: 2; + background-color: var(--contrast-hover); + color: var(--contrast-inverse); +} + +.page-link:focus { + z-index: 3; + outline: 0; + box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.25); +} + +.page-item.active .page-link { + z-index: 3; + background-color: var(--contrast); + color: var(--contrast-inverse); +} + +.page-item.disabled .page-link { + color: var(--form-element-disabled-opacity); + outline-color: var(--form-element-disabled-border-color); + pointer-events: none; + background-color: var(--form-element-disabled-background-color); } \ No newline at end of file diff --git a/src/controllers/Shop.php b/src/controllers/Shop.php index 0d11cf9..a582450 100644 --- a/src/controllers/Shop.php +++ b/src/controllers/Shop.php @@ -17,6 +17,7 @@ class Shop use Controller; private array $data; + private static int $MAX_PRODUCTS_PER_PAGE = 4; /** * Check if a product matches the category filter (if any) @@ -71,7 +72,7 @@ private function sort_product(Product $a, Product $b): int return 0; } - // sort by date + // sort by descending date if ($_GET['sort'] === 'newest') { return ($a->getCreatedDate() > $b->getCreatedDate()) ? -1 : 1; } @@ -99,6 +100,46 @@ private function sort_product(Product $a, Product $b): int return 0; // no sorting if invalid sorting option } + /** + * @return Product[] Array of products which match filters (excluding pagination) and sorting applied by user + */ + public function getMatchingProducts(): array + { + // Fetch all products from the database + $all_products = Product::getAll(); + + // Apply filtering based on search keyword and category (existing functionality) + $filtered_products = array_filter($all_products, array($this, "match_keyword")); + $filtered_products = array_filter($filtered_products, array($this, "match_category")); + + // Sort the filtered products (existing functionality) + usort($filtered_products, array($this, "sort_product")); + + return $filtered_products; + } + + /** + * @return int Page number on shop page. Defaults to 1. + */ + public function getPageNumber(): int + { + return (int)($_GET['page'] ?? 1); + } + + /** + * @param $products + * @return array Products which should be displayed on current page + */ + public function applyPagination($products): array + { + // Slice the products based on pagination + return array_slice( + $products, + ($this->getPageNumber() - 1) * Shop::$MAX_PRODUCTS_PER_PAGE, + Shop::$MAX_PRODUCTS_PER_PAGE + ); + } + public function index(): void { // check if URL follows format /shop/products/ @@ -115,22 +156,11 @@ public function index(): void return; } - // Retrieve the page number from the URL query parameters - $page = $_GET['page'] ?? 1; - $perPage = (int) $page * 4; // Number of products per page - - // Fetch all products from the database - $all_products = Product::getAll(); - - // Apply filtering based on search keyword and category (existing functionality) - $filtered_products = array_filter($all_products, array($this, "match_keyword")); - $filtered_products = array_filter($filtered_products, array($this, "match_category")); - - // Sort the filtered products (existing functionality) - usort($filtered_products, array($this, "sort_product")); + // get all products that match user criteria + $filtered_products = $this->getMatchingProducts(); // Slice the products based on pagination - $paginated_products = array_slice($filtered_products, 0, $perPage); + $paginated_products = $this->applyPagination($filtered_products); // Initialize view variables (existing functionality) $this->data['products'] = $paginated_products; @@ -138,7 +168,8 @@ public function index(): void $this->data['categories'] = Product::getCategories(); $this->data['sort_option'] = $_GET['sort'] ?? ""; $this->data['selected_categories'] = $_GET['categories'] ?? []; - $this->data['page'] = $page; + $this->data['current_page_number'] = $this->getPageNumber(); + $this->data['total_pages'] = (int)ceil(count($filtered_products) / Shop::$MAX_PRODUCTS_PER_PAGE); // Render the view with pagination information $this->view( diff --git a/src/views/Shop.php b/src/views/Shop.php index ea4cf34..c1200bf 100644 --- a/src/views/Shop.php +++ b/src/views/Shop.php @@ -10,7 +10,8 @@ * @var string[] $selected_categories Array of selected categories * @var string $search_keyword keyword used to filter products * @var string $sort_option Sort By option selected by user - * @var int $page Current page number + * @var int $current_page_number Current page number. + * @var int $total_pages Total number of pages */ use Steamy\Model\Product; @@ -32,14 +33,76 @@ function displayProduct(Product $product): void echo << $img_alt_text -
-
$name
-
Rs $price
-
+
+
$name
+
Rs $price
+
EOL; } +/** + * Returns a query string that maintains all current query string parameters and page number. + * @param int $page_number + * @return string Query string link for page item + */ +function getPageItemLink(int $page_number): string +{ + // create a string with all past query parameters except page and url + unset($_GET['page']); + unset($_GET['url']); + + $link = '?' . http_build_query($_GET); + + // add page number as query parameter + $link .= '&page=' . $page_number; + + return $link; +} + +/** + * Prints page item in HTML format. + * + * @param int $current_page_number + * @param int $page_number Page number of page item + * @return void + */ +function displayPageItem(int $current_page_number, int $page_number): void +{ + $page_link = getPageItemLink($page_number); + $className = "page-item" . ($page_number === $current_page_number ? " active" : ""); + + echo <<< EOL +
  • + $page_number +
  • + EOL; +} + +/** + * Prints navigation button in HTML format + * @param int $current_page_number + * @param int $total_pages Total number of pages + * @param bool $is_left True indicates left navigation button. + * @return void + */ +function displayNavigationButton(int $current_page_number, int $total_pages, bool $is_left): void +{ + $page_link = getPageItemLink($current_page_number + ($is_left ? -1 : 1)); + $link_content = htmlspecialchars($is_left ? "<" : ">"); + $className = "page-item"; + + if (($current_page_number === 1 && $is_left) || ($current_page_number === $total_pages && !$is_left)) { + $className .= " disabled"; + } + + echo <<< EOL +
  • + $link_content +
  • + EOL; +} + ?>
    @@ -71,12 +134,12 @@ function displayProduct(Product $product): void $sanitized_category = htmlspecialchars($category); echo <<< EOL -
  • - -
  • +
  • + +
  • EOL; } ?> @@ -96,27 +159,35 @@ function displayProduct(Product $product): void } ?> + - +
    \ No newline at end of file +