Live: https://rambalasb.github.io/vidaara/
A responsive e-commerce front end for Vidaara: ready-made earrings, necklaces, bracelets and loose beads, plus a Custom Studio where a customer builds a piece bead by bead and watches the price move as they go.
The checkout is a demo. Card fields are validated locally and the gateway call is simulated — nothing is charged and no card data is transmitted. Copy, prices and reviews are placeholders. See Checkout and payments below before putting this in front of customers.
No build step, no dependencies, no framework. Open index.html and it runs.
open index.html # straight from the filesystem
# or, so that query strings and localStorage behave exactly as in production:
python3 -m http.server 8000
| File | What it is |
|---|---|
index.html |
Home — hero, categories, featured pieces, studio teaser, craft story |
shop.html |
Listing with category / material / occasion / price filters and sort |
product.html?id=… |
Product detail — three views, specs, craft story, care, reviews |
studio.html |
Custom Studio — the five-step designer |
checkout.html |
Two-step checkout: shipping, then payment |
confirmation.html |
Order receipt |
about.html |
Workshop story, care, shipping, contact |
The cart drawer, toast and bag badge are injected into every page by js/cart.js,
so a new page only needs a [data-cart-open] button in its header.
- Base & size — necklace / bracelet / earrings, four fits each. Each fit has a bead capacity; shortening a piece trims the design and says so.
- String or wire — gold-fill, sterling, silk, nylon, leather. Earrings price the string twice, because they are a pair.
- Beads & charms — 39 beads across gemstone, glass, wood, metal and charms,
searchable by name, colour, mood, finish or material (
blue,bridal,faceted,spacer…). - Order — the sequence strip. Drag a bead to move it, or focus one and use
←/→;Deleteremoves it. Mirror repeats the sequence backwards, Undo walks back through the last 40 edits. - Finish — crafting notes, then add to bag.
The design is written to localStorage on every edit, so a half-built piece
survives a page change.
VIDAARA.quote(spec) in js/data.js is the single source of truth:
total = base + length surcharge // e.g. necklace ₹2,100 + 18" ₹320
+ string // ×2 for earrings
+ Σ bead prices // ×2 for earrings
+ hand-assembly (₹80 per bead) // ×2 for earrings
The cart, checkout and receipt all re-use the stored spec, so what the customer arranged is what the workshop receives.
Everything visual is drawn by js/render.js — palette icons, the live preview,
shop cards and cart thumbnails all come out of one engine, so nothing is a stock
photo and nothing can drift out of sync with the data.
- Seven bead shapes (round, rondelle, bicone, tube, cube, ring, drop) and four finishes (gloss, matte, faceted, metallic), each with generated gradients.
- Beads take the room their real size asks for and sit against their neighbours; the run only shrinks when the design would overflow the strand.
- Short designs cluster at the front of the strand like a real station piece; charms hang outward as pendants.
Add a bead by adding one object to V.BEADS; it appears in the studio, in
search, and renders everywhere immediately.
The storefront stays a static site. Payments and inventory are handled by a
small backend in server/ — zero npm dependencies, Node 18+ — which serves the
same files and adds /api/* on one origin, so there is no CORS to configure.
cp .env.example .env # fill in whichever keys you have
node server # http://127.0.0.1:4000
The site runs with no credentials at all. Each integration switches itself on only when every variable it needs is filled; until then the checkout behaves exactly as the demo always did. The startup banner tells you which mode each one is in.
| Path | When | What happens |
|---|---|---|
live |
Razorpay keys in .env |
Razorpay's hosted window takes the payment |
demo |
server running, no keys | full server round trip, no gateway — the flow stays testable |
offline |
no server (opened from the filesystem) | simulated in the browser, as before |
js/gateway.js asks /api/config which one applies and js/checkout.js adapts.
In live mode the card fields are hidden and replaced by Razorpay Checkout —
collecting card numbers in your own markup is what drags a site into PCI-DSS
scope, so the demo form must not survive into production.
- Dashboard → Settings → API Keys → generate the
rzp_test_pair intoRAZORPAY_KEY_ID/RAZORPAY_KEY_SECRET. - Dashboard → Settings → Webhooks → point it at
https://<your-domain>/api/razorpay/webhook, subscribe toorder.paid,payment.capturedandpayment.failed, and put the signing secret inRAZORPAY_WEBHOOK_SECRET. - Restart. The checkout switches to live mode on its own.
The flow, and why each step exists:
POST /api/quote re-price the bag from ids + specs alone
POST /api/checkout/order price again, persist a pending order, open a
Razorpay order for exactly that amount
→ Razorpay window card / UPI / net banking / wallet
POST /api/checkout/verify HMAC-verify the signature, cross-check the payment
amount, then push to Zoho
POST /api/razorpay/webhook the authoritative confirmation — fires even if the
customer closes the tab mid-redirect
The browser never sends a price. server/pricing.js recomputes every line
from js/data.js — the same file the browser loads, via server/catalogue.js —
and rejects unknown products, unknown beads and bead counts that will not fit
the chosen length. The totals rule there mirrors cart.totals() in
js/cart.js; change one and you must change the other.
Amounts go to Razorpay in paise, so a whole-rupee total is simply ×100.
Create a Self Client at api-console.zoho.in with these scopes:
ZohoInventory.salesorders.CREATE ZohoInventory.contacts.CREATE
ZohoInventory.contacts.READ ZohoInventory.items.READ
ZohoInventory.invoices.CREATE ZohoInventory.settings.READ
Put the client id, client secret, refresh token and organization id in .env,
and set ZOHO_REGION to the data centre that issued the token (in for
zoho.in) — a region mismatch is the usual cause of a token refresh failing.
Item mapping, which you set up once in Zoho:
- Ready-made pieces match by SKU:
ZOHO_PRODUCT_SKU_PREFIX+ the product id fromjs/data.js, somonsoon-tealbooks againstVD-monsoon-teal. - Studio pieces have effectively infinite permutations, so they all book
against one non-inventory item (
ZOHO_CUSTOM_ITEM_SKU, defaultVIDAARA-CUSTOM) with the rate overridden per line and the full bead recipe written into the line description. - Beads are drawn down separately as an inventory adjustment when
ZOHO_ADJUST_BEAD_STOCK=true, matching SKUBEAD-<bead id>. This is what keeps component stock honest for custom work.
ZOHO_SYNC_STOCK=true serves live levels at GET /api/stock, keyed by product
id. The endpoint works; nothing in the shop grid reads it yet.
A Zoho failure never fails a paid order. The order is stored as paid with the
error recorded, and retried by hand:
curl -H "x-admin-token: $ADMIN_TOKEN" http://localhost:4000/api/admin/orders
curl -X POST -H "x-admin-token: $ADMIN_TOKEN" \
http://localhost:4000/api/admin/orders/VD-XXXXXX/resync
Zoho does revise its field names — if a call starts failing, diff the request
body in server/zoho.js against their current v1 docs before anything else.
The checkout defaults to India. The state field is a dropdown of the 28 states
and 8 union territories held in V.INDIAN_STATES (js/data.js), spelled as
Zoho and the GST portal spell them — Zoho derives the sales order's place of
supply from that string, so a free-text state would land the tax in the wrong
place. PIN codes are validated as six digits not starting with zero, and phone
numbers as 10-digit mobiles (the field stays optional).
Choosing any other country swaps the dropdown for a free-text region box and
relaxes the postcode rule, so international orders still work. The element id
stays sh-state in both cases, so nothing downstream changes.
Both rule sets are enforced twice: in js/checkout.js for the customer, and
again in requireShipping() (server/routes.js) — which also canonicalises the
state's spelling before it reaches Zoho — because the browser's copy is a
convenience, not a control.
- Orders live in
server/data/orders.json. Fine for one node and a demo catalogue; swap the four functions inserver/store.jsfor real database calls before volume. - GST is a flat 3% estimate in
js/data.js. Once Zoho is authoritative it computes real per-item tax from HSN codes, and the site figure becomes a display estimate only. - Have someone review the payment and tax handling. This code has not taken a real payment.
css/styles.css design system — tokens, components, responsive rules
js/data.js bases, strings, beads, products, reviews, pricing
js/render.js SVG bead + jewellery renderer
js/ui.js product cards, stars, [data-piece] hydration
js/cart.js cart state, drawer, toast, persistence
js/gateway.js talks to the backend; drives Razorpay Checkout
js/studio.js the Custom Studio
js/shop.js filtering and sorting (state kept in the URL)
js/product.js product detail page
js/checkout.js two-step checkout and validation
js/confirmation.js receipt
js/site.js nav, dropdown, reveal-on-scroll
server/index.js static files + /api on one origin (no dependencies)
server/config.js .env loader; decides which integrations are live
server/catalogue.js runs js/data.js server-side — one price list, not two
server/pricing.js authoritative repricing; rejects tampered carts
server/razorpay.js order creation, signature and webhook verification
server/zoho.js OAuth, contacts, sales orders, bead stock adjustments
server/fulfilment.js idempotent post-payment sync (webhook races the browser)
server/store.js order records — swap for a database before volume
server/routes.js the API handlers
The logo is vector, so it stays sharp at any size and on any display.
| File | Where it is used |
|---|---|
img/logo.svg |
The horizontal lockup — every page header, 46px tall |
img/logo-light.svg |
The same lockup, reversed — the dark footer |
img/mark.svg |
The emblem alone, square — SVG favicon |
img/wordmark.svg |
The wordmark alone — spare, for tight or wide placements |
img/favicon.png |
128² raster favicon, fallback for older browsers |
img/logo-original.png |
The stacked raster lockup — social/OG cards, which reject SVG |
All four SVGs are derived from the supplied logo.svg by script: the opaque
background plate the trace baked in is dropped (so the logo is genuinely
transparent), the paths are split into emblem and wordmark on the empty column
band between them, and each file gets a viewBox trimmed to its own ink. That
last part is what makes the header lockup align itself — there is no padding to
compensate for.
logo-light.svg remaps the five traced teals to cream tints and warms the gold,
keeping the original depth ordering, because the teal artwork has no contrast on
the footer's #14201f.
Re-run the derivation if the master artwork changes; the colour map lives in the
script's LIGHT dictionary.
- Palette: paper
#FDFBF7, emerald#0B6E6D, gold#D4AF37, charcoal#2B2B2B— taken to match the logo's teal and gold. - Type is a system serif / system sans pairing — no webfont request, nothing to block first paint. Swap in a licensed serif when the brand fonts are chosen.
- Keyboard and screen-reader paths are wired: skip link, focus-visible rings,
labelled controls,
aria-pressedon every toggle,Escapecloses the drawer, and the bead strip is fully operable without a mouse. - Copy, prices, reviews and the workshop story are placeholders written to show the layout working. Replace them before this goes anywhere near customers.