Day 22: Useful Browser APIs
✅ Today you will learn
- URL and URLSearchParams for shareable filters
- History API: pushState, popstate
- IntersectionObserver: lazy loading, infinite scroll, reveal animations
- Clipboard, Web Share and WhatsApp deep links
- Geolocation and permissions – used responsibly
🏢 In the real world
The shop owner asks: “When I send a customer a link to milk sweets under ₹700, it should open with that filter already on.” “Can customers copy the coupon in one tap and share a product on WhatsApp?” “Can we tell how far the customer is from the shop?” All of this is built into the browser – no libraries needed.
URL and URLSearchParams
const link = new URL('https://sharmasweets.in/menu?category=milk&max=700&sort=price#list');
console.log(link.hostname, link.pathname, link.hash);
console.log(link.searchParams.get('category'), Number(link.searchParams.get('max')));
console.log(link.searchParams.has('veg'));
link.searchParams.set('max', 900); // update
link.searchParams.append('tag', 'sugar-free');
link.searchParams.delete('sort');
console.log(link.toString());
const p = new URLSearchParams({ q: 'kaju katli', city: 'Jaipur' });
console.log(p.toString()); // spaces and special characters encoded safelyShareable filters with the History API
<label>Category <select id="cat"><option value="">All</option><option value="milk">Milk</option><option value="dry">Dry</option></select></label>
<label>Max ₹ <input id="max" type="number" step="100" value="1000" style="width:90px"></label>
<p>Address bar would show: <code id="addr"></code></p>
<ul id="list"></ul>
<script>
const products = [
{ name: 'Kaju Katli', category: 'dry', price: 900 }, { name: 'Ghewar', category: 'milk', price: 650 },
{ name: 'Rasmalai', category: 'milk', price: 700 }, { name: 'Ladoo', category: 'dry', price: 500 },
];
const cat = document.querySelector('#cat'), max = document.querySelector('#max');
function render() {
const list = products.filter(p => (!cat.value || p.category === cat.value) && p.price <= Number(max.value));
document.querySelector('#list').innerHTML = list.map(p => `<li>${p.name} – ₹${p.price}</li>`).join('') || '<li>None</li>';
}
function updateUrl() {
const params = new URLSearchParams();
if (cat.value) params.set('category', cat.value);
if (max.value !== '1000') params.set('max', max.value);
const url = location.pathname + (params.toString() ? '?' + params : '');
try { history.pushState({ category: cat.value, max: max.value }, '', url); } catch (e) { /* sandboxed preview */ }
document.querySelector('#addr').textContent = '/menu' + (params.toString() ? '?' + params : '');
}
// read filters from the URL on load (works on a real page)
const start = new URLSearchParams(location.search);
if (start.get('category')) cat.value = start.get('category');
if (start.get('max')) max.value = start.get('max');
[cat, max].forEach(el => el.addEventListener('input', () => { updateUrl(); render(); }));
window.addEventListener('popstate', (e) => { // Back / Forward buttons
cat.value = e.state?.category ?? ''; max.value = e.state?.max ?? '1000'; render();
});
updateUrl(); render();
</script>IntersectionObserver: react when things scroll into view
<style>
.reveal { padding: 18px; margin: 8px 0; background: #fef3c7; border-radius: 10px; opacity: 0; transform: translateY(16px); transition: .5s; }
.reveal.show { opacity: 1; transform: none; }
</style>
<p>Scroll inside this box ↓</p>
<div id="feed" style="height:260px;overflow:auto;border:1px solid #e5e7eb;border-radius:10px;padding:10px"></div>
<script>
const feed = document.querySelector('#feed');
let page = 0;
const names = ['Kaju Katli', 'Ghewar', 'Rasmalai', 'Ladoo', 'Kalakand', 'Soan Papdi', 'Peda', 'Jalebi'];
const sentinel = document.createElement('p'); // invisible marker at the bottom
sentinel.textContent = 'Loading more…';
function addPage() {
page++;
for (let i = 0; i < 4; i++) {
const card = document.createElement('div');
card.className = 'reveal';
card.textContent = `#${(page - 1) * 4 + i + 1} ${names[((page - 1) * 4 + i) % names.length]}`;
feed.insertBefore(card, sentinel);
revealObserver.observe(card);
}
if (page >= 5) { sentinel.textContent = 'That’s all 🍬'; moreObserver.disconnect(); }
}
// 1. Reveal animation when a card enters the box
const revealObserver = new IntersectionObserver((entries) => {
entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('show'); revealObserver.unobserve(e.target); } });
}, { root: feed, threshold: 0.3 });
// 2. Infinite scroll: load the next page when the sentinel appears
const moreObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) addPage();
}, { root: feed, rootMargin: '80px' });
feed.append(sentinel);
moreObserver.observe(sentinel);
</script>| Use case | How |
|---|---|
| Lazy-load images | Observe img[data-src]; when visible set src (or simply use loading="lazy") |
| Infinite scroll | Observe a sentinel at the end of the list, fetch the next page |
| Scroll reveal animations | Add a class when an element enters the viewport |
| Analytics “seen” events | Count a banner as viewed when 50% visible for 1 s |
| Pause videos | Pause when the video leaves the screen |
scroll fires dozens of times per second and measuring positions forces layout calculations – janky on cheap phones. IntersectionObserver is calculated by the browser efficiently and only calls you when visibility actually changes.
Clipboard, Web Share and WhatsApp links
<p>Coupon: <code id="code">SWEET10</code> <button id="copy">Copy</button></p>
<p><button id="share">Share this product</button>
<a id="wa" target="_blank" rel="noopener">Order on WhatsApp</a></p>
<p id="msg" role="status"></p>
<script>
const msg = document.querySelector('#msg');
document.querySelector('#copy').addEventListener('click', async () => {
const code = document.querySelector('#code').textContent;
try {
await navigator.clipboard.writeText(code); // needs HTTPS and a user click
msg.textContent = 'Copied ✅';
} catch (err) {
msg.textContent = 'Copy blocked here – select and copy manually: ' + code;
}
});
document.querySelector('#share').addEventListener('click', async () => {
const data = { title: 'Kaju Katli – Sharma Sweets', text: 'Fresh kaju katli ₹900/kg 🍬', url: 'https://sharmasweets.in/menu#kaju' };
if (navigator.share) {
try { await navigator.share(data); msg.textContent = 'Shared!'; }
catch (err) { msg.textContent = 'Share cancelled'; }
} else {
msg.textContent = 'Web Share is not available on this device – use the WhatsApp link.';
}
});
const text = 'Namaste! I want to order 1 kg Kaju Katli for delivery tomorrow.';
document.querySelector('#wa').href = 'https://wa.me/919829012345?text=' + encodeURIComponent(text);
</script>Spaces, &, ? and Hindi text must be encoded inside URLs. encodeURIComponent turns “1 kg Kaju & Ghewar” into a safe string – forgetting it breaks WhatsApp messages at the first &.
Geolocation: distance to the shop
<button id="locate">How far am I from the shop?</button>
<p id="dist" role="status"></p>
<script>
const SHOP = { lat: 26.9196, lon: 75.8235 }; // Johari Bazaar, Jaipur
function km(a, b) { // Haversine formula
const R = 6371, rad = d => d * Math.PI / 180;
const dLat = rad(b.lat - a.lat), dLon = rad(b.lon - a.lon);
const h = Math.sin(dLat / 2) ** 2 + Math.cos(rad(a.lat)) * Math.cos(rad(b.lat)) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.asin(Math.sqrt(h));
}
document.querySelector('#locate').addEventListener('click', () => {
const out = document.querySelector('#dist');
if (!('geolocation' in navigator)) { out.textContent = 'Location not supported'; return; }
out.textContent = 'Asking for permission…';
navigator.geolocation.getCurrentPosition(
pos => {
const d = km(SHOP, { lat: pos.coords.latitude, lon: pos.coords.longitude });
out.textContent = `You are about ${d.toFixed(1)} km away. ${d <= 15 ? 'We deliver to you 🛵' : 'Courier for dry sweets 📦'}`;
},
err => { out.textContent = 'Location unavailable (' + err.message + '). Enter your PIN code instead.'; },
{ enableHighAccuracy: false, timeout: 8000, maximumAge: 600000 }
);
});
</script>| API | Needs | Good practice |
|---|---|---|
| Geolocation | HTTPS + permission | Ask only after a click, explain why, offer a PIN-code fallback |
| Clipboard write | HTTPS + user gesture | Show a “Copied” confirmation |
| Web Share | HTTPS, mainly mobile | Feature-detect and offer a fallback link |
| Notifications | Permission | Ask only after the user opts in (e.g. “Notify me when ghewar is back”) |
| Page Visibility | Nothing | Pause timers/polling when document.hidden |
🏅 Job skills unlocked today
Practice tasks
🏋️ Practice – do it yourself
Show solution
const cart = [{ name: 'Kaju Katli', qty: 2 }, { name: 'Ghewar', qty: 1 }, { name: 'Ladoo & Peda box', qty: 1 }];
const lines = ['Namaste Sharma Sweets 🙏', 'My order:', ...cart.map(i => `• ${i.name} × ${i.qty}`), 'Please confirm.'];
const url = 'https://wa.me/919829012345?text=' + encodeURIComponent(lines.join('\n'));
console.log(url);Quick quiz
🧠 Quick Quiz – test yourself
Interview corner
💼 Interview Corner
How would you implement infinite scroll?
Render the first page, place a sentinel element after the list and observe it with IntersectionObserver (with rootMargin for prefetch). When it intersects, fetch the next page (guarding against duplicate requests), append items, and stop observing at the end. Keep URLs/state for back navigation and consider virtualisation for very long lists.
Why keep filters in the URL?
URLs are shareable, bookmarkable, work with Back/Forward, survive refreshes and help SEO and analytics. URLSearchParams + history.pushState/replaceState keep UI state and URL in sync without reloads.
Summary
- URL/URLSearchParams parse and build query strings safely.
- pushState + popstate keep filters shareable without reloads.
- IntersectionObserver for lazy load, infinite scroll and reveals.
- Clipboard, Share, WhatsApp links and geolocation – with HTTPS, permission and fallbacks.
Day 23: regular expressions – validate mobile numbers, PIN codes, GSTIN, e-mail and passwords properly.
