Midterm Project — Tip Calculator MILESTONE

যা শিখেছেন সব দিয়ে একটি বাস্তব app বানান

~60 min Intermediate 5 extension tasks Real project

1. The Brief

Build a single-page Tip Calculator: enter a bill amount, pick a tip percentage, see total + per-person split. Persist last bill in localStorage. Deploy free to GitHub Pages or Netlify.

একটি ছোট কিন্তু সম্পূর্ণ project আপনার ১০০টি ছোট example-এর চেয়ে অনেক বেশি শেখাবে। এই project-এ আপনি DOM, event, state, এবং localStorage একসাথে ব্যবহার করবেন।

2. Project Structure

tip-calculator/
├─ index.html
├─ style.css
├─ app.js
└─ README.md

3. index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Tip Calculator</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <main>
        <h1>💸 Tip Calculator</h1>

        <label>Bill (৳)
            <input id="bill" type="number" min="0" step="0.01" placeholder="0.00">
        </label>

        <label>Tip %
            <div class="tips">
                <button data-tip="5">5%</button>
                <button data-tip="10" class="active">10%</button>
                <button data-tip="15">15%</button>
                <button data-tip="20">20%</button>
            </div>
        </label>

        <label>People
            <input id="people" type="number" min="1" value="1">
        </label>

        <section class="result">
            <p>Tip:    <b id="tip">৳0.00</b></p>
            <p>Total:  <b id="total">৳0.00</b></p>
            <p>Per person: <b id="per">৳0.00</b></p>
        </section>

        <button id="reset">Reset</button>
    </main>
    <script src="app.js" defer></script>
</body>
</html>

4. style.css (compact)

* { box-sizing: border-box; }
body {
    font-family: system-ui, "Hind Siliguri", sans-serif;
    background: #0f172a; color: #e2e8f0;
    display: grid; place-items: center; min-height: 100vh;
}
main {
    background: #1e293b; padding: 2rem; border-radius: 12px;
    width: min(420px, 90%); box-shadow: 0 10px 30px rgba(0,0,0,0.4);
}
h1 { margin: 0 0 1.2rem; }
label { display: block; margin: 0.8rem 0; }
input[type="number"] {
    width: 100%; padding: 0.6rem; margin-top: 0.3rem;
    border-radius: 6px; border: none; background: #334155;
    color: white; font-size: 1rem;
}
.tips { display: flex; gap: 0.4rem; margin-top: 0.4rem; }
.tips button {
    flex: 1; padding: 0.5rem; border-radius: 6px;
    border: 1px solid #475569; background: transparent; color: white;
    cursor: pointer;
}
.tips button.active { background: #39b549; border-color: #39b549; }
.result { margin: 1.2rem 0; padding: 1rem; background: #0f172a; border-radius: 8px; }
#reset {
    width: 100%; padding: 0.7rem;
    background: #ef4444; color: white; border: none;
    border-radius: 6px; cursor: pointer; font-weight: 600;
}

5. app.js — the Brain

// Single source of truth
const state = {
    bill:   0,
    tipPct: 10,
    people: 1,
};

const $ = (id) => document.getElementById(id);
const fmt = (n) => "৳" + n.toFixed(2);

// Load saved state if any
try {
    Object.assign(state, JSON.parse(localStorage.getItem("tip-state") || "{}"));
    $("bill").value   = state.bill || "";
    $("people").value = state.people;
} catch {}

function render() {
    const tip   = state.bill * state.tipPct / 100;
    const total = state.bill + tip;
    const per   = total / Math.max(1, state.people);
    $("tip").textContent   = fmt(tip);
    $("total").textContent = fmt(total);
    $("per").textContent   = fmt(per);
    document.querySelectorAll(".tips button").forEach(b =>
        b.classList.toggle("active", +b.dataset.tip === state.tipPct));
    localStorage.setItem("tip-state", JSON.stringify(state));
}

$("bill").addEventListener("input", e => {
    state.bill = +e.target.value || 0;
    render();
});
$("people").addEventListener("input", e => {
    state.people = Math.max(1, +e.target.value || 1);
    render();
});
document.querySelector(".tips").addEventListener("click", e => {
    if (e.target.tagName !== "BUTTON") return;
    state.tipPct = +e.target.dataset.tip;
    render();
});
$("reset").addEventListener("click", () => {
    Object.assign(state, { bill: 0, tipPct: 10, people: 1 });
    $("bill").value = ""; $("people").value = 1;
    render();
});

render();

6. Runnable Logic Demo

The app uses real DOM, but the compute logic can run inside this sandbox so you can test it before wiring up HTML.

tip-logic.js
function tipCalc(bill, tipPct, people) {
    const tip   = bill * tipPct / 100;
    const total = bill + tip;
    const per   = total / Math.max(1, people);
    return {
        tip:    "৳" + tip.toFixed(2),
        total:  "৳" + total.toFixed(2),
        per:    "৳" + per.toFixed(2),
    };
}

console.log(tipCalc(1500, 10, 3));
console.log(tipCalc(2200, 15, 4));

7. Deploy It

  1. Create a new GitHub repo, push the three files
  2. Go to Settings → Pages → Source: main / root
  3. Wait ~1 min — your app is live at https://<you>.github.io/<repo>/
  4. Tell five friends. Done.

8. Glossary (শব্দকোষ)

TermMeaningবাংলায়
SPASingle-Page App — one HTML page; JS swaps content.এক HTML page; JS-ই content পরিবর্তন করে।
StateThe single object that holds the app's current data.App-এর বর্তমান data ধরে রাখা single object।
RenderTurning state into DOM (called every time state changes).State থেকে DOM তৈরি — প্রতিবার state বদলালে চলে।
Event handlerFunction attached to a DOM event (click, input, submit).DOM event-এ যুক্ত function।
localStorageBrowser key-value store, persisted across reloads.Browser-এর key-value store; reload-এর পরও থাকে।
PersistenceSaving data so it survives a refresh or close.Refresh/close-এর পরও data ধরে রাখা।
DeploymentPutting your app on a public URL.App-কে public URL-এ live করা।
GitHub PagesFree static hosting from a GitHub repo.GitHub repo থেকে free static hosting।
মনে রাখবেন: ছোট app-ও যদি real user-এর কাছে পৌঁছায়, সেটিই শেখার সবচেয়ে ভালো পথ। state object + render() pattern, event listener, localStorage persistence — এই তিনটি দিয়ে অনেক বড় feature তৈরি হয়। GitHub Pages-এ deploy করে আপনার CV-তে link দিন।

9. Extension Tasks

  1. Add a custom-tip text input that lets the user enter any percentage.
    ✨ Show Answer

    Add <input id="customTip" type="number" placeholder="custom %"> and wire its input handler to set state.tipPct = +e.target.value; reuse render().

  2. Add a currency selector (BDT / USD / EUR) and reformat with Intl.NumberFormat.
    ✨ Show Answer
    a2.js
    const fmt = (n, code) =>
        new Intl.NumberFormat("en", { style: "currency", currency: code }).format(n);
    console.log(fmt(1500, "BDT"), fmt(15, "USD"), fmt(15, "EUR"));
  3. Add a tax slider 0–15% on top of the tip.
    ✨ Show Answer

    Add state.taxPct, an <input type="range" min="0" max="15"> wired to update it, and recompute total as bill + tip + bill * taxPct / 100.

  4. Add a dark/light mode toggle that persists in localStorage.
    ✨ Show Answer
    a4.js
    // Sketch — works inside any page
    const theme = {
        get() { return localStorage.getItem("theme") || "dark"; },
        toggle() {
            const next = this.get() === "dark" ? "light" : "dark";
            localStorage.setItem("theme", next);
            return next;
        }
    };
    console.log(theme.get(), theme.toggle(), theme.toggle());
  5. Deploy to GitHub Pages and share the URL with three classmates.
    ✨ Show Answer

    Steps: git init, git add ., git commit -m "init", push to a new GitHub repo, then Settings → Pages, choose main branch, root folder, save. After ~60s the URL is live. There is no #5 answer to copy — you literally have to ship it. That is the assignment.

Summary — Module 19

You shipped a real, deployed JavaScript app from scratch — a single source of truth state, event delegation, localStorage persistence, and a clean render() function. Five extension tasks await — each one teaches you something the lesson didn't.

আপনি একটি বাস্তব, deployed app বানিয়েছেন। state, event, localStorage, render — সব মিলিয়ে আধুনিক frontend-এর core ধারণা।

Next Module → The Event Loop, Stack & Queue — JS-এর হৃৎস্পন্দন।