2023 Web Development GitHub ↗

Overview

Hakimi Fruits is a website built for a local fruit and produce business to establish an online presence and let customers browse available stock before visiting or ordering. The site features a home page with a seasonal highlights banner, a products page with filterable listings organised by category (citrus, berries, tropical, seasonal), and a contact/order inquiry page. Each product card displays an image, name, price, and availability status. The design uses warm greens, oranges, and cream tones to evoke freshness and a friendly local-market character. JavaScript-powered category filtering lets customers narrow the product list without a page reload, and a CSS Grid auto-fill layout ensures product cards reflow cleanly from single-column mobile to multi-column desktop without explicit breakpoints on the grid.

Architecture

graph TD subgraph Pages["Pages"] A["index.html\nHome · Seasonal Highlights"] B["products.html\nProduct Listings + Filter"] C["contact.html\nOrder Inquiry Form"] end subgraph Styles["Styles"] D["style.css\nDesign Tokens · Layout · Components"] end subgraph JS["JavaScript"] E["main.js\nCategory Filter · Mobile Menu · Form Validation"] end subgraph Shared["Shared HTML Components (via CSS classes)"] F[".nav — shared navigation"] G[".footer — shared footer"] end A --> D B --> D C --> D B --> E C --> E A --> F B --> F C --> F A --> G B --> G C --> G style A fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style B fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style C fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style D fill:#181818,stroke:#1e1e1e,color:#888 style E fill:#181818,stroke:#1e1e1e,color:#888 style F fill:#181818,stroke:#1e1e1e,color:#888 style G fill:#181818,stroke:#1e1e1e,color:#888

Tech Stack

  • HTML5 — Semantic product cards using <article>; data-category attributes on each card drive the JavaScript filter
  • CSS3 — CSS custom properties for the warm colour palette; CSS Grid with auto-fill minmax(240px, 1fr) for the responsive product listing; Flexbox for navigation
  • Vanilla JavaScript — Category filter (show/hide cards by data-category), mobile hamburger menu toggle, order form validation
  • Responsive Design — Mobile-first base styles; breakpoints at 480 px and 768 px for typography and spacing adjustments; the product grid self-adapts via auto-fill

Build Process

1
Product Categories & Listing Structure

Defined the product taxonomy — citrus, berries, tropical, and seasonal — and planned the data attributes needed to support JavaScript filtering. Each product was given a consistent content model: image, name, price, availability badge (In Season / Limited / Out of Season), and a category tag.

2
Product Card Component

Built the product card as a reusable HTML pattern with a consistent structure. The data-category attribute on each <article> is the single source of truth for the filter logic — no classes need to change when products are recategorised.

<article class="product-card" data-category="citrus">
  <img src="images/orange.jpg" alt="Navel Oranges" loading="lazy"
       width="400" height="300">
  <div class="product-card__body">
    <h3 class="product-card__name">Navel Oranges</h3>
    <span class="product-card__price">$3.99 / kg</span>
    <span class="product-card__badge product-card__badge--in">In Season</span>
  </div>
</article>
3
CSS Grid Product Layout

Used grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)) so the number of columns adapts automatically to viewport width. This eliminates the need for breakpoints on the grid declaration itself. A gap of 1.25rem provides consistent gutters at all sizes.

.products-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 1.25rem;
  padding: var(--space-md) 0;
}

.product-card img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 4px 4px 0 0;
}
4
JavaScript Category Filter

Filter buttons have a data-filter attribute that maps to the card data-category values. On click, the script iterates all cards and sets display: none or display: '' depending on whether the category matches. An "All" button resets visibility. The active filter button receives an .is-active class for visual feedback.

const filterBtns = document.querySelectorAll('[data-filter]');
const cards = document.querySelectorAll('.product-card');

filterBtns.forEach(btn => {
  btn.addEventListener('click', () => {
    const filter = btn.dataset.filter;
    filterBtns.forEach(b => b.classList.remove('is-active'));
    btn.classList.add('is-active');
    cards.forEach(card => {
      card.style.display =
        (filter === 'all' || card.dataset.category === filter) ? '' : 'none';
    });
  });
});
5
Mobile Navigation

A hamburger button toggles an .is-open class on the <nav> element. The nav links are hidden at small viewports via display: none in the base styles and revealed as a full-width dropdown when the class is present. The toggle button is hidden at wider breakpoints using @media (min-width: 768px).

6
Contact & Order Inquiry Form

The order inquiry form includes fields for name, email, phone, preferred collection date, and a product selection textarea. JavaScript validates required fields and email format before submission. The form uses a mailto: action as the delivery mechanism, with a visible success message shown after valid submission.

Customer Browsing Flow

flowchart LR A["Customer\nLands on Home"] --> B["Views Seasonal\nHighlights"] B --> C["Goes to\nProducts Page"] C --> D{"Filter by\nCategory?"} D -->|"Yes"| E["Clicks Filter Button\nJS Hides Other Cards"] D -->|"No"| F["Browses All\nProduct Cards"] E --> G["Views Filtered\nProduct List"] F --> G G --> H["Finds Desired\nProduct"] H --> I["contact.html\nOrder Inquiry Form"] I --> J["JS Validates Fields"] J -->|"Invalid"| K["Inline Error Shown"] J -->|"Valid"| L["Inquiry Sent\nto Business"] K --> I style A fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style B fill:#181818,stroke:#1e1e1e,color:#888 style C fill:#181818,stroke:#1e1e1e,color:#888 style D fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style E fill:#181818,stroke:#1e1e1e,color:#888 style F fill:#181818,stroke:#1e1e1e,color:#888 style G fill:#181818,stroke:#1e1e1e,color:#888 style H fill:#181818,stroke:#1e1e1e,color:#888 style I fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style J fill:#181818,stroke:#1e1e1e,color:#888 style K fill:#181818,stroke:#1e1e1e,color:#888 style L fill:#1a1a2e,stroke:#00ff88,color:#e0e0e0

Challenges & Solutions

Orphan Cards in CSS Grid Auto-Fill at Mid-Range Viewports. At certain viewport widths, auto-fill with a fixed minmax minimum sometimes produced a last row with a single card that stretched to the full grid width — visually inconsistent with the multi-column rows above it. The fix was to constrain the maximum card width with a max-width on the individual card and to use justify-items: start on the grid, so orphan cards retain their natural size instead of expanding to fill the implicit column track.

Product Image Placeholder Sizing. Before final images were supplied, placeholder images of varying dimensions caused the product grid to have inconsistent card heights. Applying aspect-ratio: 4 / 3 and object-fit: cover to the <img> element resolved this and also ensured the final supplied images would always render at consistent proportions regardless of their source dimensions.

What I Learned

  • Using data-* attributes as a clean, semantic contract between HTML and JavaScript for filter interactions
  • CSS Grid auto-fill with minmax() is extremely powerful for card grids but requires understanding of how orphan cards behave at edge-case widths
  • The importance of setting aspect-ratio on images early, even with placeholder content, so the layout is stable before final assets arrive
  • Structuring JavaScript filter logic to be driven by data attributes rather than CSS classes avoids tight coupling between styling and behaviour
  • Mobile-first CSS naturally keeps the base stylesheet compact; responsive expansions are genuinely additive rather than corrective
HTML5 CSS3 JavaScript Responsive E-commerce Business Website