Skip to content
rambalasbPublic

About

Vidaara — handcrafted jewellery storefront with an interactive custom bead studio. Zero dependencies, no build step.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Vidaara — handcrafted jewellery storefront

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

Pages

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.

The Custom Studio

  1. Base & size — necklace / bracelet / earrings, four fits each. Each fit has a bead capacity; shortening a piece trims the design and says so.
  2. String or wire — gold-fill, sterling, silk, nylon, leather. Earrings price the string twice, because they are a pair.
  3. Beads & charms — 39 beads across gemstone, glass, wood, metal and charms, searchable by name, colour, mood, finish or material (blue, bridal, faceted, spacer…).
  4. Order — the sequence strip. Drag a bead to move it, or focus one and use ← / →; Delete removes it. Mirror repeats the sequence backwards, Undo walks back through the last 40 edits.
  5. 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.

Pricing

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.

The bead renderer

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.

Checkout and payments

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.

The three payment paths

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.

Razorpay

  1. Dashboard → Settings → API Keys → generate the rzp_test_ pair into RAZORPAY_KEY_ID / RAZORPAY_KEY_SECRET.
  2. Dashboard → Settings → Webhooks → point it at https://<your-domain>/api/razorpay/webhook, subscribe to order.paid, payment.captured and payment.failed, and put the signing secret in RAZORPAY_WEBHOOK_SECRET.
  3. 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.

Zoho Inventory

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 from js/data.js, so monsoon-teal books against VD-monsoon-teal.
  • Studio pieces have effectively infinite permutations, so they all book against one non-inventory item (ZOHO_CUSTOM_ITEM_SKU, default VIDAARA-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 SKU BEAD-<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.

Addresses

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.

Before this takes real money

  • Orders live in server/data/orders.json. Fine for one node and a demo catalogue; swap the four functions in server/store.js for 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.

File map

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

Brand assets

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.

Notes

  • 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-pressed on every toggle, Escape closes 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.

About

Vidaara — handcrafted jewellery storefront with an interactive custom bead studio. Zero dependencies, no build step.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages