दिन 30: Major Project 2: Cart, Checkout और Launch
✅ आज आप सीखेंगे
- Immutable updates के साथ cart logic: add, change, remove, totals
- Tabs के बीच sync रहने वाला cart drawer
- Server के नियमों से मेल खाता checkout validation
- Order POST करना, server errors सही field के पास दिखाना
- Server के गिने totals और WhatsApp link वाली receipt
- Project को test, deploy और jobs के लिए present करना
🏢 असली दुनिया में
कल ग्राहक देख सकते थे। आज उन्हें ख़रीद पाना चाहिए। ज़्यादातर beginner shops यहीं टूटती हैं: 0.75 kg जैसी quantities 0.7500000001 बन जाती हैं, browser का total bill से मेल नहीं खाता, double-click से दो orders लग जाते हैं, और ग़लत coupon पर सिर्फ़ “Error” दिखता है। आप cart और checkout ऐसे बनाएँगे कि इनमें से कुछ न हो – और फिर shop को live करके अपना सबसे मज़बूत portfolio project बनाएँगे।
Test order लगाएँ
दो मिठाइयाँ जोड़ें, cart खोलें, quantity बदलें, Checkout दबाएँ। पहले ख़ाली form भेजें, फिर FREE50 जैसा ग़लत coupon, फिर SWEET10। “Pick up” चुनें और देखें कि address field ग़ायब होता है और delivery मुफ़्त हो जाती है।
Step 1: cart.js में cart logic
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) }));
}| बारीकी | क्यों ज़रूरी |
|---|---|
| qty ?? default | ?? असली 0.25 रखता है पर undefined भर देता है (Day 2) |
| map / filter, push या splice कभी नहीं | हर बार नया array – store बदलाव पहचान सकता है |
| Math.round(x * 100) / 100 | 0.5 + 0.25 = 0.75 ही रहता है, 0.7500000001 नहीं |
| filter(line => line.qty > 0) | 250 g पर − दबाने से line हट जाती है |
| Math.min(50, …) | अटका button 900 kg order नहीं कर सकता |
Step 2: totals और cart drawer
// 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() pure function है, इसलिए इसे Day 27 की तरह Node में test कर सकते हैं। ये रहे असली नियम, उन edge cases के साथ जो client जाँचेगा:
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);
});Step 3: checkout 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';
}- हर rule true या error message लौटाता है – Day 13 और Day 23 जैसा pattern।
- Phone “+91 98290 12345”, “098290 12345” या “9829012345” मानता है – पहले spaces और country code हटाता है।
- Address में 6-digit PIN सिर्फ़ delivery के लिए; pick-up में rule pass होता है और field छिप जाता है।
- पहले ग़लत field पर focus जाता है, और aria-invalid उसकी border लाल कर देता है।
Testing में “Pick up” ने address-field.hidden = true किया पर field दिखता रहा – और ख़ाली cart में भी totals दिख रहे थे। कारण: हमारी CSS ने .field और .totals को display: grid दिया, और author CSS की कोई भी display value browser के built-in [hidden] नियम से जीत जाती है। Fix style.css में ऊपर एक line है: [hidden] { display: none !important; }। इसे याद रखें – यह interview और production दोनों में आम bug है।
Step 4: order भेजना
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';
}
}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+)." } }कोई भी DevTools खोलकर आपकी JavaScript में prices या coupon logic बदल सकता है। इसीलिए cart drawer “Estimated total” कहता है और receipt server के आँकड़े दिखाती है। Server free delivery discount के बाद लगाता है, इसलिए ₹1,600 − 10% = ₹1,440 पर delivery लगेगी – फ़ैसला server का है।
Step 5: receipt और WhatsApp
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ज़्यादातर भारतीय छोटे businesses orders WhatsApp पर पक्के करते हैं। encodeURIComponent(text) वाला wa.me link order summary पहले से लिखकर WhatsApp खोलता है – न API key, न ख़र्च। Checkout form के nodes checkoutFormNodes में रखे हैं ताकि अगला order नए form से शुरू हो।
Step 6: end-to-end test run
हमने Playwright (Day 27) से Chrome में desktop और phone width पर पूरा सफ़र अपने-आप test किया:
| Step | अपेक्षित | नतीजा |
|---|---|---|
| Products API HTTP 500 दे, फिर Try again | दोस्ताना error, फिर 8 मिठाइयाँ load | ✅ |
| Pick up चुना | Address field छिपा, delivery FREE | ✅ |
| ½ kg Kaju Katli + 1 kg Ghewar, फिर Kaju पर + | Badge 2, subtotal ₹1,325, delivery ₹60 | ✅ |
| Hint text | “Add ₹175 more for free delivery” | ✅ |
| ख़ाली checkout भेजें | Name और phone errors, name पर focus | ✅ |
| Phone “+91 98290 12345” | स्वीकार | ✅ |
| Coupon WRONG1 | Coupon के नीचे server 422 | ✅ |
| Coupon SWEET10 | Receipt: discount −₹133, total ₹1,252 | ✅ |
| Order के बाद | Cart badge 0 | ✅ |
| Dark mode, 375 px phone | पढ़ने लायक, horizontal scroll नहीं | ✅ |
| JavaScript errors | कोई नहीं | ✅ |
Step 7: shop को live करें
- 1Netlify Drop: sharma-shop folder को app.netlify.com/drop पर खींचें – 30 सेकंड में live, मुफ़्त HTTPS।
- 2GitHub Pages: folder को repository में push करें, Settings → Pages → Deploy from branch। Portfolio के लिए सबसे अच्छा क्योंकि recruiters code देखते हैं।
- 3Hostinger / cPanel: File Manager से folder को public_html/shop में upload करें। असली client domain के लिए यही।
- 4Live URL phone पर खोलें, test order लगाएँ, browser console जाँचें – फिर link share करें।
Launch checklist
- Title, meta description और theme-color set हैं (HTML course)।
- सभी images में sizes और lazy loading; Lighthouse performance 90+।
- सिर्फ़ keyboard test: search → filters → add → cart → checkout तक Tab करें।
- Wi-Fi बंद करके reload – दोस्ताना offline error दिखे।
- असली orders लेने से पहले API_URL को client की असली order API से बदलें।
- Shop का link मुख्य Sharma Sweets website menu में जोड़ें।
Step 8: jobs के लिए present करें
# 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 पूछे | इस project से आपका जवाब |
|---|---|
| “किसी project के बारे में बताइए।” | असली client समस्या, one-way data flow, live API, server-checked checkout, tested और deployed। |
| “API errors कैसे संभालते हैं?” | fetchJSON: timeout, res.ok, दोस्ताना messages, retry, 422 field errors। |
| “State कैसे manage करते हैं?” | update/subscribe वाला छोटा store; cart सिर्फ़ ids रखता है। |
| “Security?” | Data के लिए textContent, prices और coupons server पर जाँचे। |
| “Performance?” | Debounced search, एक replaceChildren, lazy images। |
🎉 आपने 30-दिन का JavaScript course पूरा किया
अगले कदम: तीनों projects (To-Do, Quiz, Sharma Sweets shop) live links के साथ GitHub पर डालें, फिर React सीखें – आप state, rendering और events पहले से समझते हैं, जो React का 70% है। उसके बाद back end (PHP या Node.js) जोड़ें ताकि shop असली orders save करे।
🏅 आज के Job Skills
Practice tasks
🏋️ Practice – ख़ुद करें
Quick quiz
🧠 Quick Quiz – ख़ुद को जाँचें
Interview corner
💼 Interview Corner
Client और server totals एक जैसे कैसे रखेंगे?
सिर्फ़ ids और quantities भेजें, server prices देखे, coupons और delivery लगाए, और अंतिम आँकड़े लौटाए। Client checkout से पहले अनुमान और बाद में server की receipt दिखाता है। साझा नियम एक जगह (या दोनों तरफ़ test) रखें ताकि कम ही अलग हों।
Form में server-side validation errors कैसे दिखाएँगे?
Input name से keyed fields object के साथ 422 लौटाएँ। catch block में fields पर loop करके matching input पर aria-invalid और उसके नीचे message लगाएँ, और बिना field वाली बात general alert area में दिखाएँ।
इसे production-ready बनाने के लिए क्या बदलेंगे?
असली order database और admin panel, payment gateway (Razorpay/UPI), stock reservation, rate limiting, analytics, error monitoring, image optimisation और CI में tests वाला modules build step।
सारांश
- Cart: immutable updates, सिर्फ़ ids, सुरक्षित decimals, totals एक pure function।
- Checkout: गति के लिए browser rules, सच के लिए server rules, हर field पर 422 errors।
- भेजते समय submit disable; सफलता के बाद ही cart ख़ाली।
- Test, deploy, README लिखें – आपकी shop अब portfolio project है।
Course पूरा! सभी lessons, practice files और तीनों projects वाला course kit download करें, फिर React या back-end course से आगे बढ़ें।
