Skip to content

Repository files navigation

TakMarket

🛒 تک‌مارکت — TakMarket

A fully static, right-to-left Persian electronics storefront, built with Vite + React and served from GitHub Pages.

33 products · 8 categories · client-side search, filters and cart · no backend, no database, no tracking.

🛍️ Open the store · 🐛 Report a bug


📑 Contents


✨ Features

🔎 A real catalogue, searched in the browser. 33 products across 8 categories, with full-text search, brand/price/stock/discount/rating filters, sorting and pagination — all running on static JSON, no API call anywhere. Every one of those choices lives in the query string, so a filtered view is a URL you can bookmark, share, refresh, or step back out of.

🧭 Right-to-left by design. Persian throughout, laid out with CSS logical properties (start/end, not left/right), Persian digit formatting for every price and count, and the self-hosted Vazirmatn typeface.

🛒 A cart and wishlist that survive a refresh. Both live in localStorage via Zustand's persist, and both treat what they read back as untrusted input — corrupt or hand-edited entries are dropped or clamped rather than rendered.

📱 Responsive from 320px up. Mobile-first layout, a slide-in menu, a scrollable category rail, and no horizontal overflow at any breakpoint.

💬 Demo support chat. A floating widget with a typing indicator and keyword-matched canned replies. It says up front that nobody is on the other end — see src/lib/chatReplies.ts.

♿ Accessible. Skip-to-content link, one <h1> per page, labelled form fields and icon buttons, visible :focus-visible rings, drawers that really trap focus instead of only claiming aria-modal, and a prefers-reduced-motion block that turns the animations off.

🔒 Hardened by default. A restrictive Content-Security-Policy meta tag ships in <head> — and the published build drops 'unsafe-inline' from script-src entirely. Forms are validated with Zod, and no component anywhere uses dangerouslySetInnerHTML.

🔗 Every URL is a real page. All 44 indexable routes are written out as their own HTML file at build time, each with its own title, description, canonical tag, social tags and JSON-LD (Product, BreadcrumbList, Organization), plus a matching sitemap.xml and a 1200×630 social card drawn from the site's own logo and typeface.


⚠️ What "no backend" means here

GitHub Pages serves static files only — no Node process, no database. The original project had an Express + MySQL backend; this build does not, and it never pretends otherwise.

Works completely ✅

Area Notes
Browsing and search Runs on static JSON, in the browser
Filtering and sorting Brand, max price, in-stock, discounted, rating
Pagination Fully client-side
Product detail pages Image gallery, spec table, related products
Cart Saved in the visitor's localStorage
Wishlist Saved in the visitor's localStorage

Deliberately doesn't ❌

These forms have complete UIs and real validation, but on submit they show a plain notice that this build has no backend, instead of faking success:

  • Login and register (/login, /register)
  • Placing an order (/checkout)
  • Newsletter signup

The admin dashboard was dropped entirely — without a server there is nothing for it to administer. No visitor data is ever sent anywhere.


🛠️ Tech stack

  • Vite 8 — bundler and dev server.
  • React 18 + React Router 7 for client-side routing.
  • TypeScript, strict mode.
  • Tailwind CSS 3 with custom design tokens (tailwind.config.ts).
  • Zustand for cart and wishlist state, persisted to localStorage.
  • Zod for form validation.
  • Vazirmatn via @fontsource-variable/vazirmatn — self-hosted, so no request ever leaves for Google's servers.
  • sharp, opentype.js and resvg in build scripts only, for thumbnails, the favicon and the social card.

Why Vite and not Next.js?

Earlier versions of this project were built with Next.js (App Router). Since the site has no backend and is published statically to GitHub Pages, the server-side half of Next.js — SSR, API routes, runtime image optimisation — was never being used. Vite produces the same final artifact with a simpler config and a much faster build.

What that gave up, and how it's paid back: Next.js pre-rendered every page at build time, so each product had its own HTML file. A plain Vite SPA has one index.html and lets React Router sort out the rest — which means every URL but the home page was served through the 404 fallback, answering with an HTTP 404 and the site-wide title. scripts/prerender.mjs puts that back: after the bundle is built it writes a real dist/<route>/index.html for all 44 indexable URLs, each carrying its own head. The body is still filled in by React — this is metadata prerendering, not server rendering — but the pages now return 200 with their own titles, which is what indexing and link previews depend on.


🚀 Installation and usage

npm install
npm run dev

Then open http://localhost:3000.

Building

npm run build

The static site lands in dist/. To preview exactly what will be published:

npm start

Scripts

Command What it does
npm run dev Regenerates assets, then starts Vite on port 3000
npm run build Regenerates assets, typechecks, builds into dist/, prerenders every route
npm start Serves the built dist/ locally
npm run lint ESLint, including the a11y rules
npm run typecheck tsc --noEmit
npm run build:images Rebuilds the 400px *-thumb.webp product thumbnails
npm run build:data Rebuilds src/data/summaries.json from products.json
npm run build:sitemap Rebuilds public/sitemap.xml
npm run build:favicon Redraws favicon.svg, favicon.ico, apple-touch-icon.png
npm run build:og Redraws public/og-image.png, the 1200×630 social card
npm run build:hero Recomposes public/hero.webp from the featured product photos

The image, data and sitemap steps run automatically before dev and build. summaries.json and sitemap.xml are generated — don't hand-edit them. The favicon, social card and hero are design assets: they are committed, and only rebuilt when you change the brand or the products they feature.


📂 Code structure

index.html                  Vite entry point; site-wide <head> tags live here
vite.config.ts              Base path, aliases, and the Pages output files
scripts/
  build-thumbnails.mjs      400px WebP thumbnails for catalogue images
  build-summaries.mjs       Generates the light product records
  build-sitemap.mjs         Generates public/sitemap.xml
  prerender.mjs             Writes one HTML file per indexable route
  build-favicon.mjs         Draws the icons from the font's own "ت" glyph
  build-og-image.mjs        Draws the 1200×630 social card
  build-hero.mjs            Composes the home page's hero image
  lib/routes.mjs            The indexable routes + their metadata and JSON-LD
  lib/woff.mjs              Unpacks a WOFF to a TTF the drawing tools can read
src/
  main.tsx                  Root render + BrowserRouter
  App.tsx                   Layout shell and route table
  routes/                   One component per page
  components/
    Link.tsx                Router link with a next/link-shaped API
    PageMeta.tsx            Per-route title, description, canonical, og:url
    ErrorBoundary.tsx       App-wide error boundary
    layout/ home/ product/ shop/ cart/ checkout/ auth/ ui/
  hooks/
    useRouter.ts            Small navigation helper
    useHydrated.ts          True once localStorage state has loaded
  store/                    Zustand cart and wishlist stores
  data/
    products.json           Full catalogue
    summaries.json          Generated light records for cards
    categories.json         Categories
  lib/
    products.ts             Reads over the full record (product page only)
    summaries.ts            Filter, sort and search over light records
    format.ts               Persian number, price and date formatting
    validation.ts           Zod schemas
    email.ts                The email rule, dependency-free (see below)
    chatReplies.ts          Canned support-chat replies
    site.ts                 Site name, URL, repo, and the demo notice

Why two data files?

File Contents Used by
products.json Full records, including descriptions and spec tables lib/products.ts only (product detail page)
summaries.json Just the fields a product card needs Everything else — listings, filters, search, cart

Descriptions and spec tables are roughly 70% of the catalogue's bytes and never appear in a listing. If a component used on a listing imported lib/products.ts, all of that would land in that route's bundle — so those components always use lib/summaries.ts.


🖼️ Adding or editing products

  1. Add the record to src/data/products.json.
  2. Drop the images into public/ as WebP.
  3. Run npm run build:images to generate the 400px thumbnails.

summaries.json and sitemap.xml regenerate on the next dev or build.


⚡ Performance notes

  • Route-level code splitting. Every route except the home page is behind React.lazy, so a visitor who never leaves the landing page never downloads the checkout schemas or the product gallery. The home page is eager on purpose — splitting it would only add a request that every first visit needs.
  • Zod is not in the first download. It used to be, because the footer's newsletter box imported the shared schemas and the footer is on every page — 14 KB gzipped to validate one text field. That form now uses src/lib/email.ts, which validation.ts also builds its email schema on, so the rule stays in one place and Zod loads only with the forms that need it.
  • Two sizes per image. Cards render at roughly 180–280 CSS px, so build-thumbnails.mjs emits a 400px WebP next to each full-size image; the large file is only fetched on a product page.
  • A hero built at the size it renders. hero.webp is composed at 1160×870, exactly twice its layout box, so it is sharp on a 2× screen and never upscaled or cropped on a 1× one.
  • Lazy images below the fold. Everything except the hero is loading="lazy", and every image sits in a fixed-aspect-ratio box, so loading one never shifts the layout.
  • Self-hosted fonts, subset by script. The Arabic and Latin subsets load separately, so a page with no Latin text doesn't pay for Latin glyphs.

A cold load of the home page is about 165 KB over 22 requests: one ~89 KB gzipped JS bundle, 8 KB of CSS, the two font subsets, and the hero image.


🛡️ Security notes

The published output is entirely static, so the whole server-side class of vulnerabilities — SQL injection, auth bypass, file upload — cannot exist here; there is no server-side code to attack. What is in place:

  • Content-Security-Policy as a <meta> tag, with object-src 'none', base-uri 'self', form-action 'none', worker-src 'none' and frame-src 'none'. The published build goes further than the source: the file in the repo allows inline script because Vite's dev server injects its HMR client that way, and cspForProduction() in vite.config.ts strips 'unsafe-inline' out of script-src when it writes dist/index.html — the production bundle has no inline script to allow.
  • No dangerouslySetInnerHTML anywhere — every string goes through React's escaping.
  • localStorage is treated as untrusted. It is fully user-writable, so every persisted cart and wishlist line is re-validated on load: negative prices and quantities, non-numeric values, duplicate ids and image paths that are not really paths (javascript:, //another-host/x.png, ..) are dropped or clamped.
  • The query string is treated the same way. The shop reads its filters, sort and page number out of the URL, so every one of them is validated against the real brands, sort keys and ratings before it is used.
  • Zod validation on every form, with rel="noopener noreferrer" on every external link.
  • No secrets in the repository — the project has no secret to keep.
  • npm audit reports 0 vulnerabilities.

Known limitation

Real HTTP security headers (X-Frame-Options, or frame-ancestors against clickjacking) can't be set on GitHub Pages: the service doesn't allow custom response headers, and browsers ignore frame-ancestors when it arrives via <meta>. Hosting that supports headers — Netlify, Vercel, Cloudflare Pages — is the fix if those matter.

Client-side validation is not a security boundary. Here it exists purely to give the visitor honest feedback, because there is no server that could be tricked into trusting the input.


🎨 Customization

Colours, spacing and shadows are design tokens in tailwind.config.ts. The brand ramp drives the header, buttons, links and the generated icons:

brand: {
  50:  "#eef4ff",
  600: "#1d45f5",   // primary actions, logo badge, favicon
  700: "#1633e1",   // hover state
  // ...
}

After changing brand-600, rerun npm run build:favicon, npm run build:og and npm run build:hero so the icons, the social card and the hero all match.

Store name, tagline, URL and contact details live in src/lib/site.ts. The phone, email and address are deliberate placeholders — replace them before using this for a real shop. site.url is read by the app, the sitemap and the prerenderer alike.

Publishing under a different path or domain takes two edits: PROJECT_BASE in vite.config.ts (or VITE_BASE_PATH at build time) for the URL prefix, and site.url above for the canonical/OG/sitemap URLs. A custom domain served from the root wants the base set to /.


📄 License

Released under the MIT License. This started life as a fork of an open-source storefront template by Aleksandar Kuzmanović; that copyright line stays in LICENSE alongside the one for this rewrite.

About

A fully static Persian electronics store built with Vite + React. Shop, filter, cart, wishlist — all in your browser, no backend. Perfect as a template or demo.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages