A pixel-for-pixel rebuild of the original static Digikala-style home page
(page/index.html + style/style.css + script/script.js) on Next.js 16
(App Router), React 19, TypeScript, and Tailwind CSS v4 — plus a
new /category/[slug] section so the site has real, multi-page routing.
Requires Node.js ≥ 20.9 (Next.js 16's minimum).
npm install
npm run devOpen http://localhost:3000. Other scripts:
npm run build # production build (Turbopack, on by default in Next 16)
npm run start # serve the production build
npm run lint # eslint .
npm run typecheck # tsc --noEmitNo manual setup steps are required beyond npm install.
| Route | Description |
|---|---|
/ |
The original home page: hero slider, deals carousel, category grids, DigiPlus/DigiClub, bestsellers, blog, etc. |
/products |
New: a full listing page — search, category/brand/price/rating/availability filters (desktop sidebar, mobile slide-over), sort, grid/list toggle, pagination, loading skeleton, empty state. 32 sample products spanning all 12 categories. |
/cart |
New: a fully functional shopping cart — quantity controls, save-for-later, coupon + gift-card codes, shipping estimate, order summary, empty/loading states, recommended & recently-viewed rows. Client-side state only (React Context + localStorage), seeded with sample items — see "The cart has no backend" below. |
/category/<slug> |
12 statically-generated category pages — mobile, digital-goods, home-kitchen, fashion, supermarket-goods, books-stationery, toys, beauty-health, sport-travel, cars-motorcycles, local-products, gift-card — one per tile in the "دستهبندیهای دیجیکالا" grid. |
/sitemap.xml |
Generated from src/app/sitemap.ts, listing the home page and all 12 category pages. |
src/
app/ # Routes, root layout, global styles, favicon, sitemap
category/<slug>/ # 12 static category route folders (see table above)
components/
layout/ # Header, Footer, TopPromoBanner (global site chrome)
ui/ # Small reusable primitives (SiteImage, PriceTag, MainSection, ...)
features/
home/ # Every home-page-only section component + HomePage.tsx
category/ # CategoryPageView + the shared route-page factory
constants/ # Literal source data (typed, one file per content area)
services/ # Thin functions in front of constants/ (the seam for a real API later)
types/ # Shared TypeScript interfaces
hooks/ # useCarousel (hero slider), useScrollToTop
lib/ # next/font/local config, generated image-dimensions manifest
utils/ # asset(), cn(), chunk() helpers
public/assets/ # All images, icons (fonts are self-hosted via next/font instead)
This project is being extended (not rebuilt) into a fuller e-commerce experience per an ongoing brief, in phases. Phases 1 (cart) and 2 (products listing) are done; see the roadmap at the end of this section for what's next.
Dependencies added beyond the original rebuild, because this phase's brief specifically calls for them:
framer-motion— item add/remove/save transitions on the cart page.- shadcn/ui-style primitives (
class-variance-authority,clsx,tailwind-merge,lucide-react, and Radix UI primitives —react-slot,react-separator,react-checkbox,react-dialog) —Button,Input,Select,Checkbox,Separator,Badge,Skeleton,Sheet,Paginationinsrc/components/ui/, alongside the existing hand-built primitives (SiteImage,PriceTag,MainSection, ...) — same folder, no conflicts. They're themed via a@themeblock at the top ofglobals.cssthat maps--color-primaryetc. to colors already used elsewhere in the site (#ef4056brand red,#eeeeeeborders,#8a8a8amuted text) instead of shadcn's default zinc palette, so new components look native rather than bolted on.components.jsondocuments the convention, even though it was hand-written here rather than run through the actualshadcnCLI (no network access in this environment to fetch it). The Sheet's open/close animation is hand-written CSS keyframes rather than thetailwindcss-animateplugin, to avoid a dependency for two rules.
The cart and product catalog have no backend, since none exists in this project:
src/features/cart/CartContext.tsxis a client-side React Context +useReducer, persisted tolocalStorage, seeded with 4 sample items. Coupon codes (DIGI10,WELCOME300) and gift-card validation (any 8+ character code) are intentionally simple stand-ins for what a real checkout/payments API would do.src/constants/catalog.tsholds 32 sample products across all 12 categories, reusing photography already in/public/assetsrather than introducing new images.src/services/productCatalogService.tsdoes the filtering/sorting client-side over that static array; swapping in a real product API means replacing that service's implementation, not the UI components that call it.- The products page's price filter is a plain min/max input pair rather than a slider — this matches how the real Digikala site's own filter UI works, and avoids taking on a dependency for a dual-thumb range control.
Neither page's header/nav links are new — /cart is reachable via the
header's cart icon (now a real link with a live item-count badge), and
/products doesn't yet have a nav entry point of its own. Wiring up
fuller header navigation is part of the "Improve Existing Components"
phase below, not bolted on ad hoc here.
Roadmap (later phases from the same brief, not yet built): an About page, Login/Register, a Framer Motion rewrite of the hero slider, and a broader visual-polish pass across the header/footer/dropdowns/inputs.
Reported with screenshots showing /category/* pages with huge, overlapping
product photos instead of a clean icon grid. Root cause: .meno-item (the
"دستهبندیهای دیجیکالا" tiles) never had an img sizing rule in the
original stylesheet — every sibling section (category-item,
digikala-segustions-item) sizes its icons to 60×60px, but this one rule
was missing, and the source photos are 718–1436px square. This was a bug in
the original site itself, faithfully (if unfortunately) preserved during the
pixel-perfect rebuild rather than caught. Fixed by adding the missing
.meno-item img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; } rule, matching its siblings.
While auditing for the same class of issue, a few other product-image
sections had a related but milder problem — sized by width only, with
height: auto or no object-fit, so cards in the same grid could end up
different heights or (if a photo's aspect ratio didn't match its box)
slightly stretched. Added object-fit: cover and, where the box's height was
implicit, a fixed aspect-ratio to: the amazing-deals carousel, the
discounted-products grid, DigiPlus's product row, and the bestselling lists.
Also added: a first pass of hover animations (soft lift via transform: translateY, shadow transitions, ~180ms ease-out) across category tiles,
product cards, the DigiClub challenges, blog cards, the cart icon, and the
shadcn Button component — the original stylesheet had exactly one hover
rule in the whole site, so this was a real gap. The new /products page's
ProductCard uses Framer Motion (whileHover) instead of CSS, per the
brief's ask to use it "where appropriate" for product cards specifically.
Styling is the original stylesheet, not a Tailwind rewrite. The brief allowed
CSS Modules or Tailwind "only if it reproduces the design exactly." The
source stylesheet uses very specific pixel values, calc() grids, and custom
breakpoints (1150/950/750/650/420px) that don't map onto Tailwind's default
scale — rewriting ~1500 lines of it into utility classes by hand risked subtle
visual drift. Instead, globals.css is the original stylesheet ported
verbatim (same selectors, same values), with Tailwind v4 layered underneath
it for any future work: its Preflight reset is intentionally skipped (via the
@import "tailwindcss/theme.css" layer(theme) / utilities.css split) so it
can never fight with the site's own reset, and the ported CSS is left
unlayered so it always wins the cascade. The new /category pages do use a
few plain CSS classes rather than Tailwind utilities too, just to stay
visually consistent with the rest of the site — Tailwind's utilities layer is
there and ready if you want to use it for new work.
Images. Every raster photo (jpg/png/webp) goes through next/image
with its real intrinsic size (captured once into
src/lib/image-manifest.ts), so you get lazy-loading, AVIF/WebP, and no
layout shift — while the rendered size is still fully controlled by CSS,
exactly as before. SVG icons and the one animated GIF banner
(gif-disprice-banner.gif) intentionally bypass next/image (see
SiteImage.tsx): SVGs gain nothing from raster optimization, and optimizing
the GIF would strip its animation.
Fonts. The custom Yekan typeface is now self-hosted via next/font/local
(src/lib/fonts.ts) instead of a hand-written @font-face. Only the .woff
file is used — every browser Next.js 16 officially supports (Chrome/Edge/Firefox
111+, Safari 16.4+) already supports WOFF, so the legacy .eot/.ttf/.svg
font files were dropped as genuine dead weight.
Filenames with spaces. Four source images had spaces in their filenames
(e.g. iphone 13pro green.webp). They were renamed with hyphens
(iphone-13pro-green.webp) for URL safety; nothing about how they look or
where they're used changed.
The 12 category pages are new, and are static folders rather than a dynamic
route. The original was a single HTML page, so there was nothing to
"preserve" here — this section exists because the project needed real,
reachable multi-page routing. It's implemented as 12 plain folders
(app/category/mobile/, app/category/digital-goods/, ...) sharing one
factory (src/features/category/categoryPageFactory.tsx) rather than a single
dynamic app/category/[slug]/ route: there's a small, fixed, known set of
exactly 12 categories, so a dynamic segment wasn't buying anything, and
avoiding bracket characters in the file tree sidesteps a real compatibility
problem — several Windows/mobile zip tools mishandle [/] in archive
entries. Rather than inventing new product photography, each page reuses real
images and price strings that already exist elsewhere on the site (the same
way the original itself reuses the same placeholder prices across many
unrelated products, and reuses the "دفتر" view-category group twice). Three
categories (home & kitchen, toys, local products) have no matching product
photos in the original asset set at all, so those honestly show an empty
state instead of forcing an unrelated image into the grid.
The brief called for an exact rebuild, which sometimes means keeping things that look like mistakes rather than fixing them:
- The footer's newsletter submit button has class
.my-btnin the source (a literal typo — the dot is inside the class name). It has no matching CSS rule either way, and the page's one.my-btnclick handler binds to the other.my-btnelement (the back-to-top button) viaquerySelector, which only ever grabs the first match. The reproduction is faithful: the newsletter button renders unstyled-by-class (it's still styled by a parent selector) and does nothing on click. - In the first "بر اساس بازدیدهای شما" row, the "کیف و کاور گوشی" group's 4th image isn't wrapped in a link, unlike its siblings — kept as-is.
- The "دفتر" (notebook) group appears twice, identically, across the two "بر اساس بازدیدهای شما" rows — a copy/paste artifact in the source, kept as-is.
- In the discounted-products grid, only 2 of the 3 rows' last tiles get a
border-left-nomodifier (row 3's doesn't) — an inconsistency in the source markup, reproduced exactly rather than "fixed" with clean modular arithmetic. - All the header's inline category links (
شگفتانگیزها,سوپرمارکت, ...) are plain, unlinked text in the source, not anchors — reproduced as bare<li>text.
These are additive and don't change anything visible, so they were made without hesitation per the brief's own accessibility section:
<html lang="fa" dir="rtl">(the source hadlang="en"despite being an entirely Persian, RTL page).- A
<main>landmark around routed content. - Full
Metadata(title template, description, Open Graph, Twitter card) and a generated SVG favicon — the original page had none of these at all. generateMetadataper category page.
metadataBase in src/app/layout.tsx is currently a placeholder
(https://dgikala-nextjs.example.com) — update it to your real domain before
deploying, and consider adding a proper 1200×630 Open Graph image (none of
the source's existing images are that shape, so one wasn't forced in).