Add guest house cost estimator

This commit is contained in:
Neeldhara Misra 2026-07-06 23:10:04 +05:30
parent b6e913b85c
commit b4a6cf6067

View file

@ -0,0 +1,544 @@
---
import SiteLayout from "../layouts/SiteLayout.astro";
const rows = Array.from({ length: 20 }, (_, index) => index + 1);
const gstRate = 0.18;
const rates = {
single: 2200,
double: 2700,
lunch: 200,
dinner: 200,
};
const withGst = (amount: number) => amount + amount * gstRate;
const currency = new Intl.NumberFormat("en-IN", {
style: "currency",
currency: "INR",
maximumFractionDigits: 0,
});
---
<SiteLayout title="Basic Calculator" description="Guest house and meal cost estimator." brandSubtitle="">
<section class="calculator-page" aria-labelledby="calculator-title">
<div class="calculator-heading">
<div>
<p class="eyebrow">Utility</p>
<h1 id="calculator-title">Basic Calculator</h1>
</div>
<button class="button button--secondary print-button" type="button" data-print-estimate>
Print Cost Estimate
</button>
</div>
<div class="calculator-shell">
<section class="calculator-panel" aria-labelledby="people-control-title">
<div class="people-control">
<div>
<h2 id="people-control-title">Number of People</h2>
<p>Use the slider to set up the estimate table.</p>
</div>
<output class="people-count" id="people-count-output" for="people-count">N = 5</output>
</div>
<label class="slider-row" for="people-count">
<span>1</span>
<input id="people-count" type="range" min="1" max="20" value="5" data-people-count />
<span>20</span>
</label>
<dl class="rate-grid" aria-label="Rates including GST">
<div>
<dt>Single Occupancy</dt>
<dd>{currency.format(withGst(rates.single))}</dd>
</div>
<div>
<dt>Double Occupancy</dt>
<dd>{currency.format(withGst(rates.double))}</dd>
</div>
<div>
<dt>Lunch</dt>
<dd>{currency.format(withGst(rates.lunch))}</dd>
</div>
<div>
<dt>Dinner</dt>
<dd>{currency.format(withGst(rates.dinner))}</dd>
</div>
</dl>
</section>
<section class="calculator-panel" aria-labelledby="estimate-table-title">
<div class="table-head">
<div>
<h2 id="estimate-table-title">Estimate</h2>
<p>Rates include 18% GST.</p>
</div>
</div>
<div class="calculator-table-wrap">
<table class="calculator-table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Single Occupancy</th>
<th scope="col">Double Occupancy</th>
<th scope="col">Lunch</th>
<th scope="col">Dinner</th>
</tr>
</thead>
<tbody>
{
rows.map((row) => (
<tr data-estimate-row data-row-index={row}>
<td>
<label class="sr-only" for={`person-${row}`}>Person {row} name</label>
<input
id={`person-${row}`}
class="name-input"
type="text"
placeholder={`Person ${row}`}
autocomplete="off"
spellcheck="false"
data-lpignore="true"
data-1p-ignore
/>
</td>
<td>
<label class="check-cell">
<input
type="checkbox"
value="single"
data-cost="single"
data-accommodation
aria-label={`Single occupancy for person ${row}`}
/>
<span>Single</span>
</label>
</td>
<td>
<label class="check-cell">
<input
type="checkbox"
value="double"
data-cost="double"
data-accommodation
aria-label={`Double occupancy for person ${row}`}
/>
<span>Double</span>
</label>
</td>
<td>
<label class="check-cell">
<input type="checkbox" value="lunch" data-cost="lunch" aria-label={`Lunch for person ${row}`} />
<span>Lunch</span>
</label>
</td>
<td>
<label class="check-cell">
<input type="checkbox" value="dinner" data-cost="dinner" aria-label={`Dinner for person ${row}`} />
<span>Dinner</span>
</label>
</td>
</tr>
))
}
</tbody>
</table>
</div>
<div class="total-bar" aria-live="polite">
<div>
<span>Total Cost</span>
<strong data-total-cost>{currency.format(0)}</strong>
</div>
<p data-total-note>For 5 people.</p>
</div>
</section>
</div>
</section>
<script>
const rateMap = {
single: 2200 * 1.18,
double: 2700 * 1.18,
lunch: 200 * 1.18,
dinner: 200 * 1.18,
};
const currencyFormatter = new Intl.NumberFormat("en-IN", {
style: "currency",
currency: "INR",
maximumFractionDigits: 0,
});
const peopleInput = document.querySelector<HTMLInputElement>("[data-people-count]");
const peopleOutput = document.querySelector<HTMLOutputElement>("#people-count-output");
const rows = Array.from(document.querySelectorAll<HTMLTableRowElement>("[data-estimate-row]"));
const totalCost = document.querySelector<HTMLElement>("[data-total-cost]");
const totalNote = document.querySelector<HTMLElement>("[data-total-note]");
const printButton = document.querySelector<HTMLButtonElement>("[data-print-estimate]");
function getPeopleCount() {
return Number(peopleInput?.value ?? 1);
}
function syncRows() {
const peopleCount = getPeopleCount();
rows.forEach((row, index) => {
const visible = index < peopleCount;
row.hidden = !visible;
row.querySelectorAll<HTMLInputElement>("input").forEach((input) => {
input.disabled = !visible;
});
});
if (peopleOutput) {
peopleOutput.value = String(peopleCount);
peopleOutput.textContent = `N = ${peopleCount}`;
}
if (totalNote) totalNote.textContent = `For ${peopleCount} ${peopleCount === 1 ? "person" : "people"}.`;
}
function updateTotal() {
let total = 0;
rows.forEach((row) => {
if (row.hidden) return;
row.querySelectorAll<HTMLInputElement>("input[type='checkbox']:checked").forEach((input) => {
const cost = input.dataset.cost as keyof typeof rateMap | undefined;
if (cost) total += rateMap[cost];
});
});
if (totalCost) totalCost.textContent = currencyFormatter.format(total);
}
peopleInput?.addEventListener("input", () => {
syncRows();
updateTotal();
});
rows.forEach((row) => {
row.addEventListener("change", (event) => {
const target = event.target;
if (!(target instanceof HTMLInputElement)) return;
if (target.matches("[data-accommodation]") && target.checked) {
row.querySelectorAll<HTMLInputElement>("[data-accommodation]").forEach((input) => {
if (input !== target) input.checked = false;
});
}
updateTotal();
});
});
printButton?.addEventListener("click", () => {
window.print();
});
syncRows();
updateTotal();
</script>
<style is:global>
.calculator-page {
width: min(1180px, calc(100% - 2rem));
margin: 0 auto;
padding: 3rem 0 4rem;
}
.calculator-heading {
display: flex;
align-items: end;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.4rem;
}
.calculator-heading h1 {
font-size: clamp(2.2rem, 5vw, 3.4rem);
}
.calculator-shell {
display: grid;
gap: 1rem;
}
.calculator-panel {
border-radius: 8px;
background: color-mix(in oklch, var(--card) 96%, white);
box-shadow: var(--shadow-soft);
padding: 1.1rem;
}
.people-control,
.table-head {
display: flex;
align-items: start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.people-control h2,
.table-head h2 {
font-size: 1.55rem;
}
.people-control p,
.table-head p,
.total-bar p {
color: var(--muted);
font-size: 0.95rem;
line-height: 1.35;
}
.people-count {
min-width: 4.2rem;
border-radius: 8px;
background: color-mix(in oklch, var(--secondary) 38%, white);
color: var(--foreground);
font-family: var(--font-heading);
font-size: 2rem;
line-height: 1;
padding: 0.62rem 0.75rem;
text-align: center;
font-variant-numeric: tabular-nums;
box-shadow: 0 0 0 1px color-mix(in oklch, var(--secondary) 58%, var(--border));
}
.slider-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.85rem;
color: var(--muted);
font-size: 0.9rem;
font-weight: 700;
}
.slider-row input {
width: 100%;
min-height: 44px;
accent-color: var(--primary);
}
.rate-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.65rem;
margin-top: 1rem;
}
.rate-grid div {
border-radius: 8px;
background: color-mix(in oklch, var(--muted-2) 52%, white);
padding: 0.72rem 0.8rem;
box-shadow: 0 0 0 1px color-mix(in oklch, var(--border) 70%, transparent);
}
.rate-grid dt {
color: var(--muted);
font-size: 0.78rem;
font-weight: 700;
line-height: 1.2;
}
.rate-grid dd {
margin: 0.25rem 0 0;
color: var(--foreground);
font-size: 1.1rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.calculator-table-wrap {
overflow-x: auto;
border-radius: 8px;
box-shadow: 0 0 0 1px var(--border);
}
.calculator-table {
min-width: 760px;
background: var(--card);
}
.calculator-table th {
background: color-mix(in oklch, var(--muted-2) 62%, white);
color: color-mix(in oklch, var(--foreground) 65%, var(--muted));
white-space: nowrap;
}
.calculator-table td {
padding: 0.6rem 0.7rem;
vertical-align: middle;
}
.name-input {
width: 100%;
min-height: 44px;
min-width: 13rem;
border: 0;
border-radius: 6px;
background: color-mix(in oklch, var(--muted-2) 34%, white);
color: var(--foreground);
font: inherit;
font-size: 16px;
padding: 0.52rem 0.65rem;
box-shadow: 0 0 0 1px color-mix(in oklch, var(--border) 74%, transparent);
}
.name-input:focus {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
.check-cell {
display: inline-flex;
min-height: 44px;
align-items: center;
gap: 0.5rem;
cursor: pointer;
color: color-mix(in oklch, var(--foreground) 78%, var(--muted));
font-size: 0.92rem;
font-weight: 700;
line-height: 1;
white-space: nowrap;
}
.check-cell input {
width: 1.1rem;
height: 1.1rem;
margin: 0;
accent-color: var(--primary);
}
.total-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-top: 1rem;
border-radius: 8px;
background:
linear-gradient(135deg, rgba(49, 91, 75, 0.1), rgba(240, 214, 141, 0.24)),
var(--card);
padding: 1rem;
box-shadow: 0 0 0 1px color-mix(in oklch, var(--primary) 12%, transparent);
}
.total-bar span {
display: block;
color: var(--muted);
font-size: 0.82rem;
font-weight: 700;
text-transform: uppercase;
}
.total-bar strong {
display: block;
margin-top: 0.15rem;
font-family: var(--font-heading);
font-size: clamp(2rem, 5vw, 3rem);
font-weight: 400;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
clip-path: inset(50%);
}
@media (max-width: 760px) {
.calculator-heading,
.people-control,
.total-bar {
align-items: stretch;
flex-direction: column;
}
.print-button {
width: 100%;
}
.rate-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 520px) {
.rate-grid {
grid-template-columns: 1fr;
}
}
@media print {
.site-header,
.site-footer,
.print-button,
.slider-row {
display: none !important;
}
body {
background: #fff !important;
}
.calculator-page {
width: 100%;
padding: 0;
}
.calculator-panel {
box-shadow: none;
padding: 0.5rem 0;
}
.calculator-shell {
gap: 0.45rem;
}
.rate-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.calculator-table-wrap {
overflow: visible;
box-shadow: 0 0 0 1px #ddd;
}
.calculator-table {
min-width: 0;
}
.calculator-table th,
.calculator-table td {
padding: 0.35rem 0.4rem;
}
.check-cell span {
display: none;
}
.name-input {
min-height: 0;
box-shadow: none;
background: transparent;
padding: 0;
}
.total-bar {
box-shadow: none;
border: 1px solid #ddd;
}
}
</style>
</SiteLayout>