Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
🚀 Days 28–30 – Projects  ·  Day 30

Day 30: Major Project 2: Cart, Checkout & Launch

⏱ 4 hours📶 Major project🎯 100% self-study
Day 30 / 30

✅ Today you will learn

  • Cart logic with immutable updates: add, change, remove, totals
  • A cart drawer that stays in sync across tabs
  • Checkout validation that matches the server’s rules
  • POST the order, show server errors next to the right field
  • A receipt with server-calculated totals and a WhatsApp link
  • Test, deploy and present the project for jobs

🏢 In the real world

Yesterday customers could browse. Today they must be able to buy. This is where most beginner shops break: quantities like 0.75 kg turn into 0.7500000001, the total in the browser does not match the bill, a double-click places two orders, and an invalid coupon just shows “Error”. You will build the cart and checkout so that none of this happens – and then put the shop live and turn it into your strongest portfolio project.

Place a test order

Add two sweets, open the cart, change a quantity, click Checkout. Try submitting the empty form, then a wrong coupon like FREE50, then SWEET10. Switch to “Pick up” and watch the address field disappear and delivery become free.

▶  Sharma Sweets – cart and checkout

Step 1: cart logic in cart.js

JavaScript – cart.js: add, change, remove
const FREE_DELIVERY_FROM = 1500;
const DELIVERY_FEE = 60;

function stepFor(product) {
  return product.unit === 'box' ? 1 : 0.25;            // boxes in 1s, sweets in 250 g steps
}

function addToCart(id, qty) {
  const product = store.get().products.find((p) => p.id === id);
  if (!product || product.stock === 0) return;
  const amount = qty ?? (product.unit === 'box' ? 1 : 0.5);
  store.update((s) => {
    const existing = s.cart.find((line) => line.id === id);
    const cart = existing
      ? s.cart.map((line) => (line.id === id ? { ...line, qty: Math.min(50, line.qty + amount) } : line))
      : [...s.cart, { id, qty: amount }];
    return { cart };
  });
  const badge = $('#cart-count');
  badge.classList.remove('bump');
  void badge.offsetWidth;                               // restart the CSS animation
  badge.classList.add('bump');
  toast(`Added ${formatQty(amount, product.unit)} ${product.name} 🛒`);
}

function changeQty(id, direction) {
  const product = store.get().products.find((p) => p.id === id);
  if (!product) return;
  const step = stepFor(product) * direction;
  store.update((s) => ({
    cart: s.cart
      .map((line) => (line.id === id ? { ...line, qty: Math.round((line.qty + step) * 100) / 100 } : line))
      .filter((line) => line.qty > 0),                  // reaching 0 removes the line
  }));
}

function removeFromCart(id) {
  store.update((s) => ({ cart: s.cart.filter((line) => line.id !== id) }));
}
DetailWhy it matters
qty ?? default?? keeps a real 0.25 but fills in undefined (Day 2)
map / filter, never push or spliceA new array every time – the store can detect change
Math.round(x * 100) / 1000.5 + 0.25 stays 0.75, no 0.7500000001
filter(line => line.qty > 0)Pressing − at 250 g removes the line
Math.min(50, …)A stuck button cannot order 900 kg

Step 2: totals and the cart drawer

JavaScript – cart.js: cartLines(), cartTotals(), renderCart()
// joins cart ids with product data; unknown ids (product removed) are skipped
function cartLines({ cart, products }) {
  return cart
    .map((line) => ({ ...line, product: products.find((p) => p.id === line.id) }))
    .filter((line) => line.product);
}

function cartTotals(state) {
  const lines = cartLines(state);
  const subtotal = lines.reduce((sum, l) => sum + l.product.price * l.qty, 0);
  const delivery = subtotal === 0 || subtotal >= FREE_DELIVERY_FROM ? 0 : DELIVERY_FEE;
  const count = lines.length;
  return { lines, subtotal, delivery, total: subtotal + delivery, count };
}

function renderCart(state) {
  const { lines, subtotal, delivery, total, count } = cartTotals(state);
  $('#cart-count').textContent = count;

  const list = $('#cart-list');
  list.replaceChildren(...lines.map(({ id, qty, product }) => {
    const li = document.createElement('li');
    li.className = 'cart-item';
    li.dataset.id = id;
    const img = document.createElement('img');
    img.src = product.image;
    img.alt = '';
    const info = document.createElement('div');
    const name = document.createElement('div');
    name.className = 'cart-item__name';
    name.textContent = product.name;
    const stepper = document.createElement('div');
    stepper.className = 'stepper';
    stepper.innerHTML = '<button type="button" data-action="dec" aria-label="Less">−</button><span></span><button type="button" data-action="inc" aria-label="More">+</button>';
    stepper.querySelector('span').textContent = formatQty(qty, product.unit);
    const remove = document.createElement('button');
    remove.type = 'button';
    remove.className = 'remove';
    remove.dataset.action = 'remove';
    remove.textContent = 'Remove';
    info.append(name, stepper, ' ', remove);
    const amount = document.createElement('strong');
    amount.textContent = rupees(product.price * qty);
    li.append(img, info, amount);
    return li;
  }));

  $('#cart-empty').hidden = count > 0;
  $('#cart-totals').hidden = count === 0;
  $('#t-subtotal').textContent = rupees(subtotal);
  $('#t-delivery').textContent = delivery === 0 ? 'FREE' : rupees(delivery);
  $('#t-total').textContent = rupees(total);
  $('#t-hint').textContent = subtotal > 0 && subtotal < FREE_DELIVERY_FROM
    ? `Add ${rupees(FREE_DELIVERY_FROM - subtotal)} more for free delivery 🚚`
    : subtotal > 0 ? 'You get free delivery 🎉' : '';
}

cartTotals() is a pure function, so we can test it in Node exactly like Day 27. Here are the real rules with the edge cases a client would check:

JavaScript
import { test } from 'node:test';
import assert from 'node:assert/strict';

const FREE_DELIVERY_FROM = 1500, DELIVERY_FEE = 60;
const products = [
  { id: 1, name: 'Kaju Katli', price: 900, unit: 'kg' },
  { id: 7, name: 'Classic Gift Box', price: 480, unit: 'box' },
];
function cartTotals({ cart, products }) {
  const lines = cart.map((l) => ({ ...l, product: products.find((p) => p.id === l.id) })).filter((l) => l.product);
  const subtotal = lines.reduce((sum, l) => sum + l.product.price * l.qty, 0);
  const delivery = subtotal === 0 || subtotal >= FREE_DELIVERY_FROM ? 0 : DELIVERY_FEE;
  return { subtotal, delivery, total: subtotal + delivery, count: lines.length };
}

test('empty cart costs nothing', () => {
  assert.deepEqual(cartTotals({ cart: [], products }), { subtotal: 0, delivery: 0, total: 0, count: 0 });
});
test('small order pays ₹60 delivery', () => {
  assert.equal(cartTotals({ cart: [{ id: 1, qty: 0.5 }], products }).total, 510);
});
test('exactly ₹1,500 gets free delivery', () => {
  const t = cartTotals({ cart: [{ id: 1, qty: 1 }, { id: 7, qty: 1.25 }], products });
  assert.equal(t.subtotal, 1500);
  assert.equal(t.delivery, 0);
});
test('a removed product in an old saved cart is ignored', () => {
  assert.equal(cartTotals({ cart: [{ id: 99, qty: 1 }, { id: 7, qty: 2 }], products }).count, 1);
});
✔ empty cart costs nothing (1.337844ms) ✔ small order pays ₹60 delivery (0.241871ms) ✔ exactly ₹1,500 gets free delivery (0.136793ms) ✔ a removed product in an old saved cart is ignored (1.523909ms) ℹ tests 4 ℹ suites 0 ℹ pass 4 ℹ fail 0 ℹ cancelled 0 ℹ skipped 0 ℹ todo 0 ℹ duration_ms 9.723535

Step 3: checkout validation

JavaScript – checkout.js: rules and validation
const checkoutRules = {
  name: (v) => v.trim().length >= 2 || 'Please enter your full name.',
  phone: (v) => /^[6-9]\d{9}$/.test(v.replace(/\D/g, '').replace(/^(91|0)(?=\d{10}$)/, '')) || 'Enter a 10-digit mobile number starting with 6–9.',
  address: (v, form) => form.mode.value === 'pickup' || /\b[1-9]\d{5}\b/.test(v) || 'Add your full address including the 6-digit PIN code.',
  coupon: (v) => v.trim() === '' || /^[A-Za-z0-9]{4,12}$/.test(v.trim()) || 'Coupon codes have 4–12 letters or digits.',
};

function setFieldError(form, field, message) {
  const input = form.elements[field];
  input.setAttribute('aria-invalid', message ? 'true' : 'false');
  $(`#co-${field}-err`).textContent = message || '';
}

function validateCheckout(form) {
  let firstBad = null;
  for (const [field, rule] of Object.entries(checkoutRules)) {
    const result = rule(form.elements[field].value, form);
    setFieldError(form, field, result === true ? '' : result);
    if (result !== true && !firstBad) firstBad = field;
  }
  if (firstBad) form.elements[firstBad].focus();
  return !firstBad;
}

function openCheckout() {
  const { count } = cartTotals(store.get());
  if (count === 0) return;
  $('#cart-dialog').close();
  resetCheckoutForm();
  $('#checkout-dialog').showModal();
  $('#co-name').focus();
}

function resetCheckoutForm() {
  const body = $('#checkout-body');
  if (!$('#checkout-form')) body.replaceChildren(...checkoutFormNodes);   // after a successful order
  $('#co-form-err').textContent = '';
  updateAddressVisibility();
}

function updateAddressVisibility() {
  const form = $('#checkout-form');
  if (form) $('#address-field').hidden = form.mode.value === 'pickup';
}
  • Each rule returns true or an error message – the same pattern as Day 13 and Day 23.
  • Phone accepts “+91 98290 12345”, “098290 12345” or “9829012345” – it strips spaces and the country code first.
  • Address needs a 6-digit PIN only for delivery; for pick-up the rule passes and the field is hidden.
  • The first invalid field gets focus, and aria-invalid turns its border red.
🐞 A real bug we hit: hidden did not hide

While testing, “Pick up” set address-field.hidden = true but the field stayed visible – and an empty cart still showed its totals. The reason: our CSS gave .field and .totals display: grid, and any author CSS display value beats the browser’s built-in [hidden] rule. The fix is one line at the top of style.css: [hidden] { display: none !important; }. Remember this – it is a very common interview and production bug.

Step 4: sending the order

JavaScript – checkout.js: submitCheckout()
async function submitCheckout(event) {
  event.preventDefault();
  const form = event.currentTarget;
  if (!validateCheckout(form)) return;

  const button = $('#place-order');
  button.disabled = true;                                   // no double orders
  button.textContent = 'Placing order…';
  $('#co-form-err').textContent = '';
  const data = Object.fromEntries(new FormData(form));
  const items = store.get().cart.map(({ id, qty }) => ({ id, qty }));

  try {
    const receipt = await placeOrder({ ...data, phone: data.phone.replace(/\D/g, '').slice(-10), coupon: data.coupon.trim(), items });
    store.update({ cart: [] });                              // empty the cart only after success
    showReceipt(receipt, data);
  } catch (err) {
    if (err.status === 422) {
      for (const [field, message] of Object.entries(err.fields)) {
        if (form.elements[field]) setFieldError(form, field, message);
        else $('#co-form-err').textContent = message;
      }
    } else {
      $('#co-form-err').textContent = err.message;
    }
  } finally {
    button.disabled = false;
    button.textContent = 'Place order';
  }
}
Validate in the browser (fast feedback)Disable the button – no double ordersPOST only ids + quantitiesServer checks stock, phone and coupon and calculates prices201 → empty cart, show receipt422 → show each message under its field
The order journey
What the browser sends and receives
POST https://codingeasily.in/api/sweets/
{ "name": "Aarti Gupta", "phone": "9829012345", "mode": "delivery",
  "address": "12 MI Road, Jaipur 302001", "coupon": "SWEET10",
  "items": [ { "id": 1, "qty": 0.75 }, { "id": 2, "qty": 1 } ] }

← 201 Created
{ "orderId": "SS2610258370",
  "items": [ { "id": 1, "name": "Kaju Katli", "qty": 0.75, "amount": 675 },
             { "id": 2, "name": "Malai Ghewar", "qty": 1, "amount": 650 } ],
  "subtotal": 1325, "discount": 133, "delivery": 60, "total": 1252,
  "message": "Order received (practice API – nothing was stored)." }

← 422 (wrong coupon)
{ "error": "Please fix the highlighted fields.",
  "fields": { "coupon": "Coupon not valid (SWEET10 works on orders of ₹500+)." } }
⚠️ The browser total is only an estimate

Anyone can open DevTools and change prices or the coupon logic in your JavaScript. That is why the cart drawer says “Estimated total” and the receipt shows the server’s numbers. The server also applies free delivery after the discount, so ₹1,600 − 10% = ₹1,440 still pays delivery – the server decides.

Step 5: receipt and WhatsApp

JavaScript – checkout.js: showReceipt()
function showReceipt(receipt, data) {
  const box = document.createElement('div');
  box.className = 'success';
  const unitOf = (id) => (store.get().products.find((p) => p.id === id) || {}).unit;
  const rows = receipt.items.map((i) => [`${i.name} – ${formatQty(i.qty, unitOf(i.id))}`, rupees(i.amount)]);
  rows.push(['Subtotal', rupees(receipt.subtotal)]);
  if (receipt.discount) rows.push(['Coupon discount', '−' + rupees(receipt.discount)]);
  rows.push([data.mode === 'pickup' ? 'Pick-up' : 'Delivery', receipt.delivery ? rupees(receipt.delivery) : 'FREE']);
  rows.push(['Total to pay', rupees(receipt.total)]);

  const table = document.createElement('table');
  for (const [label, value] of rows) {
    const tr = table.insertRow();
    tr.insertCell().textContent = label;
    tr.insertCell().textContent = value;
  }
  const big = document.createElement('p');
  big.className = 'big';
  big.textContent = '🎉';
  const title = document.createElement('h2');
  title.textContent = `Order ${receipt.orderId} placed!`;
  const note = document.createElement('p');
  note.textContent = `Thank you, ${data.name.trim()}. We will call you shortly to confirm. (${receipt.message})`;

  const text = [`Namaste Sharma Sweets 🙏`, `Order ${receipt.orderId}`, ...receipt.items.map((i) => `• ${i.name} – ${formatQty(i.qty, unitOf(i.id))}`), `Total ${rupees(receipt.total)}`].join('\n');
  const wa = document.createElement('a');
  wa.className = 'btn btn--block';
  wa.href = 'https://wa.me/919829012345?text=' + encodeURIComponent(text);
  wa.target = '_blank';
  wa.rel = 'noopener';
  wa.textContent = 'Send order on WhatsApp';

  box.append(big, title, note, table, wa);
  $('#checkout-body').replaceChildren(box);
  toast('Order placed successfully ✅');
}

let checkoutFormNodes = [];       // the original form, kept to restore it after a receipt

Most Indian small businesses confirm orders on WhatsApp. A wa.me link with encodeURIComponent(text) opens WhatsApp with the order summary already typed – no API key, no cost. The checkout form nodes are kept in checkoutFormNodes so the next order starts with a fresh form.

Step 6: end-to-end test run

We tested the whole journey automatically with Playwright (Day 27) in Chrome at desktop and phone width:

StepExpectedResult
Products API returns HTTP 500, then Try againFriendly error, then 8 sweets load✅
Pick up selectedAddress field hidden, delivery FREE✅
Add ½ kg Kaju Katli + 1 kg Ghewar, then + on KajuBadge 2, subtotal ₹1,325, delivery ₹60✅
Hint text“Add ₹175 more for free delivery”✅
Submit empty checkoutName and phone errors, focus on name✅
Phone “+91 98290 12345”Accepted✅
Coupon WRONG1Server 422 shown under Coupon✅
Coupon SWEET10Receipt: discount −₹133, total ₹1,252✅
After the orderCart badge 0✅
Dark mode, 375 px phoneReadable, no horizontal scroll✅
JavaScript errorsNone✅

Step 7: put the shop live

  1. 1Netlify Drop: drag the sharma-shop folder onto app.netlify.com/drop – live in 30 seconds, free HTTPS.
  2. 2GitHub Pages: push the folder to a repository, Settings → Pages → Deploy from branch. Best for your portfolio because recruiters see the code.
  3. 3Hostinger / cPanel: upload the folder to public_html/shop with File Manager. Use this for a real client domain.
  4. 4Open the live URL on your phone, place a test order, check the browser console – then share the link.
Three free or cheap ways to go live

Launch checklist

  • Title, meta description and theme-color are set (HTML course).
  • All images have sizes and lazy loading; Lighthouse performance 90+.
  • Keyboard-only test: Tab through search → filters → add → cart → checkout.
  • Turn Wi-Fi off and reload – the friendly offline error appears.
  • Swap API_URL to the client’s real order API before taking real orders.
  • Add the shop link to the main Sharma Sweets website menu.

Step 8: present it for jobs

README.md for your GitHub repository
# Sharma Sweets – Online Shop (Vanilla JavaScript)

Live demo: https://<your-username>.github.io/sharma-shop/

A mobile-first sweet-shop store for a real Jaipur business, built without frameworks.

## Features
- Live product API with loading skeleton, offline/timeout errors and retry
- Debounced search, category/price/stock filters, sorting, shareable URL filters
- Cart with 250 g steps, saved in localStorage and synced across tabs
- Checkout validation (Indian mobile, PIN code), server-side coupon and totals
- Order receipt + one-tap WhatsApp confirmation
- Accessible dialogs, keyboard shortcuts ("/" to search), dark mode

## Tech
HTML5, CSS (Grid, custom properties, :has, dialog), JavaScript (ES2022, fetch,
AbortController, URLSearchParams, Intl.NumberFormat), node:test, Playwright

## Structure
js/utils.js · api.js · store.js · catalogue.js · cart.js · checkout.js · app.js
Interviewer asksYour answer from this project
“Tell me about a project.”Real client problem, one-way data flow, live API, server-checked checkout, tested and deployed.
“How do you handle API errors?”fetchJSON: timeout, res.ok, friendly messages, retry, 422 field errors.
“How do you manage state?”A tiny store with update/subscribe; cart stores only ids.
“Security?”textContent for data, prices and coupons checked on the server.
“Performance?”Debounced search, single replaceChildren, lazy images.

🎉 You finished the 30-day JavaScript course

🧱
Days 1–9Language basics: variables, conditions, loops, functions, arrays, objects, strings.
🖱️
Days 10–15The DOM, events, forms, storage and your To-Do app.
⚙️
Days 16–21Closures, classes, errors, async/await, fetch and modules.
🚀
Days 22–27Browser APIs, regex, performance, security and testing.
🏆
Days 28–30Quiz app and a complete online shop for your portfolio.
Your 30-day journey

Next steps: put all three projects (To-Do, Quiz, Sharma Sweets shop) on GitHub with live links, then learn React – you already understand state, rendering and events, which is 70% of React. After that, add a back end (PHP or Node.js) so the shop saves real orders.

🏅 Job skills unlocked today

✔ Write cart logic with immutable updates and safe decimal maths✔ Validate forms in the browser and map server errors to fields✔ Prevent double submits and trust only server totals✔ Test pure functions and full user journeys✔ Deploy a front-end project and present it in interviews

Practice tasks

🏋️ Practice – do it yourself

Task 1: Add a “Gift message” textarea (max 120 characters with a live counter) that appears only when a gift box is in the cart.
Task 2: Add a delivery time-slot select (10–12, 12–2, 4–6, 6–8) and disable slots that have already passed today using Date.
Task 3: Save the last 5 orders (id, date, total) in localStorage and show them in a “My orders” dialog.
Task 4: Write node:test tests for checkoutRules.phone and checkoutRules.address with at least 8 cases.
Task 5: Deploy the shop on GitHub Pages and add the live link and README to your portfolio.

Quick quiz

🧠 Quick Quiz – test yourself

1. Why does the cart use map/filter instead of push/splice?
New state object each time.
2. Why disable the Place order button during the request?
Re-enabled in finally.
3. Which status does the API use for validation errors?
Unprocessable content.
4. Whose total is final?
Browser code can be changed by anyone.
5. Why Math.round(qty * 100) / 100?
Binary fractions.

Interview corner

💼 Interview Corner

How do you keep client and server totals consistent?

Send only ids and quantities, let the server look up prices, apply coupons and delivery, and return the final numbers. The client shows an estimate before checkout and the server receipt after. Shared rules can be kept in one place (or tested on both sides) so they rarely disagree.

How do you show server-side validation errors in a form?

Return 422 with a fields object keyed by input name. In the catch block loop over the fields, set aria-invalid and the message under the matching input, and show anything without a field in a general alert area.

What would you change to make this production-ready?

A real order database and admin panel, payment gateway (Razorpay/UPI), stock reservation, rate limiting, analytics, error monitoring, image optimisation and a build step with modules and tests in CI.

Summary

  • Cart: immutable updates, ids only, safe decimals, totals as a pure function.
  • Checkout: browser rules for speed, server rules for truth, 422 errors per field.
  • Disable submit while sending; empty the cart only after success.
  • Test, deploy, write a README – your shop is now a portfolio project.
➡️ Next

Course complete! Download the course kit with all lessons, practice files and the three projects, then continue with React or a back-end course.

Frequently Asked Questions

Does the practice API really save orders?
No. It validates and calculates like a real server but stores nothing, so you can test freely. For a real shop, build the order API in PHP or Node.js with a database.
How do I accept online payment?
Add Razorpay or a UPI payment link: the server creates a payment order, the browser opens the checkout, and the server verifies the payment signature before confirming the order.
Can I sell this shop to local businesses?
Yes – it is your code. Change the products API, colours and WhatsApp number. Many freelancers charge ₹5,000–₹15,000 for such a store plus yearly hosting.
← JavaScript (30 Days)
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Course complete! Continue withNext.js + React Your progress: 30 / 30 (100%) · Recommended next course Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free