Hakimi Fruits
Online presence for a fruit and produce business with product listings
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
Tech Stack
- HTML5 — Semantic product cards using
<article>;data-categoryattributes 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
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.
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>
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;
}
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';
});
});
});
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).
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
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-fillwithminmax()is extremely powerful for card grids but requires understanding of how orphan cards behave at edge-case widths - The importance of setting
aspect-ratioon 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