Master JavaScript through hands-on project-based assignments. Build a Calculator, Quiz App, Expense Tracker, Form Validator, Image Gallery, Countdown Timer, Rock-Paper-Scissors, Shopping Cart, Bookmark Manager, and Pomodoro Timer with complete starter code and expected outputs.
Welcome to the ultimate JavaScript practice guide! These 10 project-based assignments are designed to strengthen your JavaScript skills through real-world applications. Each assignment includes a description, requirements, hints, starter code, and expected output behavior.
How to use these assignments: Start from Assignment 1 and progress sequentially. Each project builds upon concepts from the previous ones. Try to complete each assignment independently before looking at hints.
Assignment 2: Interactive Quiz App
Description
Create a multiple-choice quiz application that presents questions one at a time, tracks the user's score, provides immediate feedback on answers, and shows a final results summary with the option to retry.
Requirements / Features
- Array of at least 10 quiz questions with 4 options each
- Display one question at a time with a progress indicator
- Highlight correct/incorrect answers after selection
- Track and display current score
- Timer for each question (15 seconds)
- Final score screen with percentage and performance message
- Retry button to restart the quiz
- Randomize question order on each attempt
Hints
- Store questions as an array of objects with
question, options, and correctAnswer properties - Use
setTimeout or setInterval for the countdown timer - Use CSS classes to toggle between correct (green) and incorrect (red) feedback
- Fisher-Yates shuffle algorithm for randomizing questions
Starter Code
// quizApp.js
const quizData = [
{
question: "What does 'typeof null' return in JavaScript?",
options: ["null", "undefined", "object", "number"],
correctAnswer: 2
},
{
question: "Which method converts JSON string to a JavaScript object?",
options: ["JSON.stringify()", "JSON.parse()", "JSON.convert()", "JSON.toObject()"],
correctAnswer: 1
},
// TODO: Add 8 more questions
];
class QuizApp {
constructor(container) {
this.container = container;
this.questions = [];
this.currentIndex = 0;
this.score = 0;
this.timer = null;
this.timeLeft = 15;
}
start() {
this.questions = this.shuffleQuestions([...quizData]);
this.currentIndex = 0;
this.score = 0;
this.showQuestion();
}
shuffleQuestions(array) {
// TODO: Implement Fisher-Yates shuffle
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
showQuestion() {
// TODO: Render current question and options
// TODO: Start countdown timer
// TODO: Update progress indicator
}
selectAnswer(selectedIndex) {
// TODO: Check if answer is correct
// TODO: Highlight correct/incorrect
// TODO: Update score
// TODO: Move to next question after delay
}
startTimer() {
this.timeLeft = 15;
// TODO: Implement countdown using setInterval
}
showResults() {
// TODO: Display final score, percentage, and retry button
}
}
const quizContainer = document.getElementById('quiz-container');
const quiz = new QuizApp(quizContainer);
quiz.start();
// Expected Behavior:
// Question 1/10 displayed with 4 options and 15s timer
// User selects correct answer → option turns green, score: 1/1
// After 1.5s delay → next question appears
// User selects wrong answer → selected turns red, correct turns green
// Timer runs out → auto-mark as incorrect, move to next
// After last question → "You scored 7/10 (70%) - Good Job!" with Retry button
Assignment 3: Expense Tracker
Description
Build an expense tracker application that allows users to add income and expenses, categorize transactions, view balance summary, and persist data using localStorage. Include filtering and basic statistics.
Requirements / Features
- Add transactions with description, amount, category, and date
- Separate income and expense entries
- Display running balance, total income, and total expenses
- Categorize expenses (Food, Transport, Entertainment, Bills, Other)
- Filter transactions by category or date range
- Delete individual transactions
- Data persistence using localStorage
- Visual summary showing expense breakdown
Hints
- Use
localStorage.setItem() and localStorage.getItem() for persistence - Store transactions as an array of objects in localStorage
- Use
Array.filter() for category/date filtering - Use
Array.reduce() to calculate totals - Format currency with
toLocaleString('en-IN') for Indian format
Starter Code
// expenseTracker.js
class ExpenseTracker {
constructor() {
this.transactions = this.loadFromStorage();
this.categories = ['Food', 'Transport', 'Entertainment', 'Bills', 'Other'];
this.init();
}
init() {
this.renderTransactions();
this.updateSummary();
document.getElementById('transaction-form').addEventListener('submit', (e) => {
e.preventDefault();
this.addTransaction();
});
}
loadFromStorage() {
// TODO: Load transactions from localStorage
const data = localStorage.getItem('expenses');
return data ? JSON.parse(data) : [];
}
saveToStorage() {
// TODO: Save transactions to localStorage
localStorage.setItem('expenses', JSON.stringify(this.transactions));
}
addTransaction() {
const transaction = {
id: Date.now(),
description: document.getElementById('description').value,
amount: parseFloat(document.getElementById('amount').value),
type: document.getElementById('type').value, // 'income' or 'expense'
category: document.getElementById('category').value,
date: document.getElementById('date').value
};
// TODO: Validate and add transaction
// TODO: Update UI and save to storage
}
deleteTransaction(id) {
// TODO: Remove transaction by id
// TODO: Update UI and storage
}
getBalance() {
// TODO: Calculate balance using reduce
return this.transactions.reduce((acc, t) => {
return t.type === 'income' ? acc + t.amount : acc - t.amount;
}, 0);
}
filterByCategory(category) {
// TODO: Filter and display transactions by category
}
filterByDateRange(startDate, endDate) {
// TODO: Filter transactions within date range
}
updateSummary() {
// TODO: Update balance, total income, total expenses display
}
renderTransactions(list = this.transactions) {
// TODO: Render transaction list in the DOM
}
}
const tracker = new ExpenseTracker();
// Expected Behavior:
// User adds: "Salary" | ₹50,000 | Income | 2026-06-01
// Balance: ₹50,000 | Income: ₹50,000 | Expenses: ₹0
// User adds: "Groceries" | ₹2,500 | Expense | Food | 2026-06-02
// Balance: ₹47,500 | Income: ₹50,000 | Expenses: ₹2,500
// User filters by "Food" → shows only food-related transactions
// User refreshes page → all data persists from localStorage
// User deletes "Groceries" → Balance updates to ₹50,000
Description
Create a comprehensive form validation system that validates user registration data in real-time. Implement both inline validation (as user types) and submission validation with custom error messages and visual indicators.
Requirements / Features
- Fields: Full Name, Email, Phone, Password, Confirm Password, Age, Website URL
- Real-time validation as user types (debounced)
- Custom error messages for each validation rule
- Visual indicators (green border for valid, red for invalid)
- Password strength meter (Weak, Medium, Strong)
- Validate email format using regex
- Phone number format validation (10 digits)
- Prevent form submission if any field is invalid
- Show success message on valid submission
Hints
- Use
input event with debounce for real-time validation - Email regex:
/^[^\s@]+@[^\s@]+\.[^\s@]+$/ - Implement debounce with
setTimeout and clearTimeout - Password strength: check length, uppercase, lowercase, numbers, special chars
- Use CSS transitions for smooth visual feedback
Starter Code
// formValidator.js
class FormValidator {
constructor(formElement) {
this.form = formElement;
this.errors = {};
this.debounceTimers = {};
this.init();
}
init() {
this.form.querySelectorAll('input').forEach(input => {
input.addEventListener('input', (e) => {
this.debounceValidation(e.target);
});
});
this.form.addEventListener('submit', (e) => {
e.preventDefault();
this.validateAll();
});
}
debounceValidation(input, delay = 500) {
clearTimeout(this.debounceTimers[input.name]);
this.debounceTimers[input.name] = setTimeout(() => {
this.validateField(input);
}, delay);
}
validateField(input) {
const { name, value } = input;
let error = '';
switch(name) {
case 'fullName':
error = this.validateName(value);
break;
case 'email':
error = this.validateEmail(value);
break;
case 'phone':
error = this.validatePhone(value);
break;
case 'password':
error = this.validatePassword(value);
this.updatePasswordStrength(value);
break;
case 'confirmPassword':
error = this.validateConfirmPassword(value);
break;
case 'age':
error = this.validateAge(value);
break;
case 'website':
error = this.validateURL(value);
break;
}
this.showFieldStatus(input, error);
}
validateName(value) {
if (!value.trim()) return 'Full name is required';
if (value.trim().length < 3) return 'Name must be at least 3 characters';
if (!/^[a-zA-Z\s]+$/.test(value)) return 'Name can only contain letters and spaces';
return '';
}
validateEmail(value) {
// TODO: Implement email validation with regex
if (!value) return 'Email is required';
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value) ? '' : 'Please enter a valid email address';
}
validatePhone(value) {
// TODO: Validate 10-digit phone number
}
validatePassword(value) {
// TODO: Check minimum length, complexity requirements
}
validateConfirmPassword(value) {
// TODO: Match with password field
}
validateAge(value) {
// TODO: Validate age is between 13 and 120
}
validateURL(value) {
// TODO: Validate URL format (optional field)
}
updatePasswordStrength(password) {
// TODO: Calculate and display password strength
// Weak: < 6 chars
// Medium: 6-8 chars with mixed case
// Strong: 8+ chars with mixed case, numbers, and special chars
}
showFieldStatus(input, error) {
// TODO: Add/remove CSS classes and show error message
}
validateAll() {
// TODO: Validate all fields and submit if valid
}
}
const form = document.getElementById('registration-form');
const validator = new FormValidator(form);
// Expected Behavior:
// User types "Jo" in name field → red border, "Name must be at least 3 characters"
// User types "John Doe" → green border, error disappears
// User types "invalid-email" → red border, "Please enter a valid email address"
// User types "john@example.com" → green border
// User types password "abc" → Strength: Weak (red bar)
// User types password "Abc123!@" → Strength: Strong (green bar)
// User submits with all valid fields → "Registration successful!" message
// User submits with errors → scroll to first error field
Assignment 5: Image Gallery with Lightbox
Description
Build a responsive image gallery with a lightbox feature. Users should be able to view thumbnails in a grid, click to open full-size images in a modal overlay, navigate between images, and filter by categories.
Requirements / Features
- Responsive grid layout for thumbnail display
- Lightbox modal with full-size image view
- Navigation arrows (previous/next) in lightbox
- Keyboard navigation (Arrow keys, Escape to close)
- Image categories with filter buttons
- Smooth CSS transitions for opening/closing lightbox
- Image lazy loading for performance
- Touch/swipe support for mobile navigation
- Image counter showing current position (e.g., "3 of 12")
Hints
- Use CSS Grid or Flexbox for the responsive gallery layout
- Create the lightbox as a fixed-position overlay with z-index
- Use
Intersection Observer API for lazy loading images - Add
keydown event listener for keyboard navigation - Track touch start/end positions for swipe detection
Starter Code
// imageGallery.js
const galleryData = [
{ id: 1, src: 'images/nature-1.jpg', thumb: 'images/thumb-1.jpg', category: 'Nature', title: 'Mountain Sunrise' },
{ id: 2, src: 'images/city-1.jpg', thumb: 'images/thumb-2.jpg', category: 'City', title: 'Urban Skyline' },
{ id: 3, src: 'images/animal-1.jpg', thumb: 'images/thumb-3.jpg', category: 'Animals', title: 'Wild Fox' },
// TODO: Add more images
];
class ImageGallery {
constructor(container) {
this.container = container;
this.images = galleryData;
this.filteredImages = [...this.images];
this.currentIndex = 0;
this.lightboxOpen = false;
this.touchStartX = 0;
this.init();
}
init() {
this.renderFilters();
this.renderGallery();
this.createLightbox();
this.setupKeyboardNavigation();
this.setupLazyLoading();
}
renderFilters() {
const categories = ['All', ...new Set(this.images.map(img => img.category))];
// TODO: Create filter buttons for each category
}
filterByCategory(category) {
this.filteredImages = category === 'All'
? [...this.images]
: this.images.filter(img => img.category === category);
this.renderGallery();
}
renderGallery() {
// TODO: Render image grid with thumbnails
// Add click handlers to open lightbox
}
createLightbox() {
// TODO: Create lightbox DOM elements
// Include: overlay, image container, close button, nav arrows, counter
}
openLightbox(index) {
this.currentIndex = index;
this.lightboxOpen = true;
// TODO: Show lightbox with selected image
// TODO: Add body overflow hidden
}
closeLightbox() {
this.lightboxOpen = false;
// TODO: Hide lightbox with transition
}
navigateImage(direction) {
// direction: 'prev' or 'next'
// TODO: Update current index and display image
// TODO: Handle wrap-around (last → first, first → last)
}
setupKeyboardNavigation() {
document.addEventListener('keydown', (e) => {
if (!this.lightboxOpen) return;
// TODO: Handle ArrowLeft, ArrowRight, Escape
});
}
setupLazyLoading() {
// TODO: Use IntersectionObserver to lazy-load thumbnail images
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
}
setupTouchNavigation() {
// TODO: Detect swipe left/right on lightbox for mobile
}
}
const galleryContainer = document.getElementById('gallery');
const gallery = new ImageGallery(galleryContainer);
// Expected Behavior:
// Page loads → 12 thumbnails displayed in responsive grid (3-4 per row)
// User clicks "Nature" filter → only Nature images shown
// User clicks thumbnail → lightbox opens with full image, "1 of 5"
// User presses Right Arrow → next image, "2 of 5"
// User presses Escape → lightbox closes smoothly
// User on mobile swipes left → next image displayed
// Images below fold load only when scrolled into view
Assignment 6: Countdown Timer
Description
Create a versatile countdown timer that allows users to set a target date/time, displays the remaining time in days, hours, minutes, and seconds, and provides visual and audio notifications when the countdown reaches zero.
Requirements / Features
- Input fields for target date and time selection
- Real-time countdown display (Days, Hours, Minutes, Seconds)
- Multiple preset timers (5 min, 15 min, 30 min, 1 hour)
- Animated circular progress indicator
- Pause and Resume functionality
- Audio notification when timer completes
- Option to set a custom title/label for the timer
- Save recent timers to localStorage for quick access
- Visual color change as timer approaches zero (last 10 seconds)
Hints
- Use
setInterval with 1000ms for updating every second - Calculate remaining time:
targetDate - Date.now() - Convert milliseconds: days =
Math.floor(ms / 86400000), etc. - Use
requestAnimationFrame for smooth progress ring animation - Use Web Audio API or
new Audio() for completion sound
Starter Code
// countdownTimer.js
class CountdownTimer {
constructor(container) {
this.container = container;
this.targetTime = null;
this.intervalId = null;
this.isPaused = false;
this.remainingMs = 0;
this.timerLabel = '';
this.recentTimers = this.loadRecentTimers();
this.init();
}
init() {
this.renderUI();
this.renderPresets();
this.renderRecentTimers();
}
renderUI() {
// TODO: Create timer display with days, hours, minutes, seconds
// TODO: Create input fields and control buttons
}
setCountdown(targetDate, label = 'Timer') {
this.targetTime = new Date(targetDate).getTime();
this.timerLabel = label;
this.isPaused = false;
this.startCountdown();
this.saveToRecent(targetDate, label);
}
setDuration(minutes, label = '') {
// TODO: Set timer from duration (for presets)
const target = Date.now() + minutes * 60 * 1000;
this.setCountdown(new Date(target), label || `${minutes} min timer`);
}
startCountdown() {
if (this.intervalId) clearInterval(this.intervalId);
this.intervalId = setInterval(() => {
if (this.isPaused) return;
const now = Date.now();
this.remainingMs = this.targetTime - now;
if (this.remainingMs <= 0) {
this.timerComplete();
return;
}
this.updateDisplay();
this.updateProgressRing();
this.checkWarningState();
}, 1000);
}
pause() {
// TODO: Pause the timer, store remaining time
this.isPaused = true;
this.remainingMs = this.targetTime - Date.now();
}
resume() {
// TODO: Resume from paused state
this.isPaused = false;
this.targetTime = Date.now() + this.remainingMs;
}
updateDisplay() {
const { days, hours, minutes, seconds } = this.parseMilliseconds(this.remainingMs);
// TODO: Update DOM elements with calculated values
}
parseMilliseconds(ms) {
return {
days: Math.floor(ms / (1000 * 60 * 60 * 24)),
hours: Math.floor((ms % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
minutes: Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60)),
seconds: Math.floor((ms % (1000 * 60)) / 1000)
};
}
updateProgressRing() {
// TODO: Update SVG circle stroke-dashoffset based on remaining time
}
checkWarningState() {
// TODO: Add warning class when remaining time < 10 seconds
}
timerComplete() {
clearInterval(this.intervalId);
// TODO: Play audio notification
// TODO: Show completion animation/message
this.playAlarm();
}
playAlarm() {
// TODO: Play notification sound
const audio = new Audio('alarm.mp3');
audio.play().catch(() => console.log('Audio autoplay blocked'));
}
loadRecentTimers() {
return JSON.parse(localStorage.getItem('recentTimers') || '[]');
}
saveToRecent(targetDate, label) {
// TODO: Save timer to recent list in localStorage
}
renderPresets() {
// TODO: Render preset duration buttons
}
renderRecentTimers() {
// TODO: Display list of recently used timers
}
}
const timerContainer = document.getElementById('timer-app');
const timer = new CountdownTimer(timerContainer);
// Expected Behavior:
// User selects date "2026-12-31 23:59:59" with label "New Year"
// Display: 203 Days | 14 Hours | 23 Minutes | 45 Seconds (updating every second)
// User clicks "5 min" preset
// Display: 0 Days | 0 Hours | 5 Minutes | 0 Seconds (counting down)
// User clicks Pause → timer freezes at current value
// User clicks Resume → countdown continues from paused value
// Timer reaches 10 seconds → display turns red/warning
// Timer reaches 0 → alarm sound plays, "Time's Up!" message appears
// Page reload → recent timers list shows "New Year" and "5 min timer"
Assignment 7: Rock-Paper-Scissors Game
Description
Build an enhanced Rock-Paper-Scissors game with animated choices, score tracking, game history, difficulty levels (random vs. pattern-based AI), and a best-of-N rounds mode.
Requirements / Features
- Player choice selection with animated hand icons
- Computer AI with two modes: Random and Smart (pattern detection)
- Score tracking: Player wins, Computer wins, Draws
- Best-of-N rounds mode (Best of 3, 5, or 7)
- Game history log showing each round's choices and result
- Animated reveal of computer's choice
- Win/Loss streak tracking
- Statistics dashboard (win rate, most chosen option)
- Reset game functionality
- Sound effects for win/lose/draw
Hints
- Use
Math.random() for random AI; track player patterns for smart AI - Result logic:
(playerChoice - computerChoice + 3) % 3 maps to win/lose/draw - Use CSS animations or
@keyframes for the hand shake reveal - Store game history in an array and render as a list
- Smart AI: track last 5 player moves, counter the most frequent
Starter Code
// rockPaperScissors.js
class RPSGame {
constructor(container) {
this.container = container;
this.choices = ['rock', 'paper', 'scissors'];
this.score = { player: 0, computer: 0, draws: 0 };
this.history = [];
this.playerPattern = [];
this.difficulty = 'random'; // 'random' or 'smart'
this.bestOf = 5;
this.currentRound = 0;
this.streak = { type: null, count: 0 };
this.init();
}
init() {
this.renderUI();
this.setupEventListeners();
}
renderUI() {
// TODO: Create choice buttons, score display, history panel
}
setupEventListeners() {
// TODO: Add click handlers for rock, paper, scissors buttons
// TODO: Add handlers for difficulty and bestOf settings
}
play(playerChoice) {
const computerChoice = this.getComputerChoice();
const result = this.determineWinner(playerChoice, computerChoice);
this.currentRound++;
this.updateScore(result);
this.updateStreak(result);
this.addToHistory(playerChoice, computerChoice, result);
this.playerPattern.push(playerChoice);
this.animateReveal(playerChoice, computerChoice, result);
this.checkGameEnd();
}
getComputerChoice() {
if (this.difficulty === 'random') {
return this.choices[Math.floor(Math.random() * 3)];
}
// Smart AI: counter player's most frequent recent choice
return this.smartChoice();
}
smartChoice() {
// TODO: Analyze last 5 player moves
// TODO: Return the choice that beats player's most common pick
const recent = this.playerPattern.slice(-5);
if (recent.length < 3) return this.choices[Math.floor(Math.random() * 3)];
const frequency = { rock: 0, paper: 0, scissors: 0 };
recent.forEach(choice => frequency[choice]++);
const mostCommon = Object.keys(frequency).reduce((a, b) =>
frequency[a] > frequency[b] ? a : b
);
// Counter the most common choice
const counters = { rock: 'paper', paper: 'scissors', scissors: 'rock' };
return counters[mostCommon];
}
determineWinner(player, computer) {
if (player === computer) return 'draw';
const wins = { rock: 'scissors', paper: 'rock', scissors: 'paper' };
return wins[player] === computer ? 'player' : 'computer';
}
updateScore(result) {
if (result === 'draw') this.score.draws++;
else this.score[result]++;
// TODO: Update score display
}
updateStreak(result) {
// TODO: Track win/loss streaks
}
animateReveal(playerChoice, computerChoice, result) {
// TODO: Animate hand shake and reveal choices
}
addToHistory(playerChoice, computerChoice, result) {
this.history.unshift({ round: this.currentRound, playerChoice, computerChoice, result });
// TODO: Update history display
}
checkGameEnd() {
const winsNeeded = Math.ceil(this.bestOf / 2);
if (this.score.player >= winsNeeded || this.score.computer >= winsNeeded) {
this.endGame();
}
}
endGame() {
// TODO: Display game over screen with final stats
}
getStatistics() {
// TODO: Calculate win rate, most picked choice, longest streak
}
resetGame() {
this.score = { player: 0, computer: 0, draws: 0 };
this.history = [];
this.currentRound = 0;
this.playerPattern = [];
// TODO: Reset UI
}
}
const gameContainer = document.getElementById('rps-game');
const game = new RPSGame(gameContainer);
// Expected Behavior:
// User clicks "Rock" → hands animate shaking → reveals: You: Rock, CPU: Scissors
// Result: "You Win! 🎉" | Score: Player 1 - Computer 0 - Draws 0
// User plays 5 rounds in Best-of-5 mode
// Player reaches 3 wins → "🏆 You Won the Match! (3-1)"
// Smart mode: Player picks Rock 4 times in a row
// Computer starts picking Paper to counter
// Statistics: Win Rate: 60% | Most Picked: Rock (45%) | Best Streak: 3 Wins
Description
Build a complete shopping cart system with product listing, add-to-cart functionality, quantity management, price calculation with discounts, and a checkout summary. Data should persist across page refreshes.
Requirements / Features
- Product catalog with at least 8 products (image, name, price, description)
- Add to cart with quantity selection
- Cart sidebar/page showing all added items
- Increase/decrease quantity buttons in cart
- Remove individual items from cart
- Apply discount/coupon codes (e.g., "SAVE10" for 10% off)
- Calculate subtotal, discount, tax (18% GST), and grand total
- Cart badge showing total item count
- Empty cart state with "Continue Shopping" button
- Persist cart data in localStorage
Hints
- Separate product data from cart state management
- Use a Map or object with product IDs as keys for quick cart lookups
- Dispatch custom events when cart changes to update all UI components
- Format prices consistently with
Intl.NumberFormat - Validate coupon codes against a predefined list
Starter Code
// shoppingCart.js
const products = [
{ id: 1, name: 'Wireless Headphones', price: 2499, image: 'headphones.jpg', description: 'Premium noise-cancelling Bluetooth headphones' },
{ id: 2, name: 'Mechanical Keyboard', price: 3999, image: 'keyboard.jpg', description: 'RGB backlit mechanical gaming keyboard' },
{ id: 3, name: 'USB-C Hub', price: 1299, image: 'hub.jpg', description: '7-in-1 USB-C multiport adapter' },
{ id: 4, name: 'Webcam HD', price: 1899, image: 'webcam.jpg', description: '1080p HD webcam with mic' },
// TODO: Add 4 more products
];
const validCoupons = {
'SAVE10': { type: 'percentage', value: 10 },
'FLAT200': { type: 'flat', value: 200 },
'WELCOME': { type: 'percentage', value: 15 }
};
class ShoppingCart {
constructor() {
this.cart = this.loadCart();
this.appliedCoupon = null;
this.taxRate = 0.18; // 18% GST
this.init();
}
init() {
this.renderProducts();
this.renderCart();
this.updateCartBadge();
}
loadCart() {
return JSON.parse(localStorage.getItem('shoppingCart') || '[]');
}
saveCart() {
localStorage.setItem('shoppingCart', JSON.stringify(this.cart));
this.dispatchCartUpdate();
}
dispatchCartUpdate() {
// TODO: Dispatch custom event for UI updates
window.dispatchEvent(new CustomEvent('cartUpdated', { detail: this.cart }));
}
renderProducts() {
// TODO: Render product grid with "Add to Cart" buttons
}
addToCart(productId, quantity = 1) {
const existingItem = this.cart.find(item => item.productId === productId);
if (existingItem) {
existingItem.quantity += quantity;
} else {
const product = products.find(p => p.id === productId);
this.cart.push({ productId, name: product.name, price: product.price, quantity });
}
this.saveCart();
this.renderCart();
this.updateCartBadge();
}
removeFromCart(productId) {
// TODO: Remove item from cart
this.cart = this.cart.filter(item => item.productId !== productId);
this.saveCart();
this.renderCart();
}
updateQuantity(productId, newQuantity) {
// TODO: Update quantity, remove if 0
if (newQuantity <= 0) {
this.removeFromCart(productId);
return;
}
const item = this.cart.find(i => i.productId === productId);
if (item) item.quantity = newQuantity;
this.saveCart();
this.renderCart();
}
applyCoupon(code) {
// TODO: Validate coupon code and apply discount
const coupon = validCoupons[code.toUpperCase()];
if (!coupon) return { success: false, message: 'Invalid coupon code' };
this.appliedCoupon = { code: code.toUpperCase(), ...coupon };
this.renderCart();
return { success: true, message: `Coupon "${code}" applied!` };
}
getSubtotal() {
return this.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
}
getDiscount() {
if (!this.appliedCoupon) return 0;
const subtotal = this.getSubtotal();
return this.appliedCoupon.type === 'percentage'
? subtotal * (this.appliedCoupon.value / 100)
: this.appliedCoupon.value;
}
getTax() {
return (this.getSubtotal() - this.getDiscount()) * this.taxRate;
}
getGrandTotal() {
return this.getSubtotal() - this.getDiscount() + this.getTax();
}
getTotalItems() {
return this.cart.reduce((sum, item) => sum + item.quantity, 0);
}
updateCartBadge() {
// TODO: Update cart icon badge with total items count
}
renderCart() {
// TODO: Render cart items with quantity controls
// TODO: Show subtotal, discount, tax, grand total
// TODO: Handle empty cart state
}
formatPrice(amount) {
return new Intl.NumberFormat('en-IN', {
style: 'currency', currency: 'INR'
}).format(amount);
}
}
const cart = new ShoppingCart();
// Expected Behavior:
// Product grid displays 8 products with prices and "Add to Cart" buttons
// User adds "Wireless Headphones" → Cart badge shows "1"
// User adds same item again → quantity becomes 2, badge shows "2"
// User opens cart sidebar:
// Wireless Headphones × 2 ₹4,998
// [−] [2] [+] [Remove]
// Subtotal: ₹4,998
// Discount: ₹0
// GST (18%): ₹899.64
// Grand Total: ₹5,897.64
// User applies "SAVE10" coupon:
// Subtotal: ₹4,998
// Discount (10%): -₹499.80
// GST (18%): ₹809.68
// Grand Total: ₹5,307.88
// Page refresh → cart persists with all items
Assignment 9: Bookmark Manager
Description
Create a bookmark manager application that allows users to save, organize, categorize, search, and manage web bookmarks. Include features like drag-and-drop reordering, tag-based filtering, and import/export functionality.
Requirements / Features
- Add bookmarks with URL, title, description, and tags
- Automatic favicon fetching from URL
- Organize bookmarks into folders/categories
- Tag-based filtering system (multiple tags per bookmark)
- Search bookmarks by title, URL, or tags
- Drag-and-drop reordering within categories
- Edit and delete existing bookmarks
- Import/Export bookmarks as JSON file
- Open bookmark in new tab
- Display bookmarks in grid or list view toggle
- Persist all data using localStorage
Hints
- Use
new URL(url).origin + '/favicon.ico' for favicon or Google's favicon service - Implement drag-and-drop using HTML5 Drag and Drop API (
dragstart, dragover, drop) - Use
Array.filter() with multiple conditions for search - Create a
Blob and URL.createObjectURL for JSON export/download - Use
FileReader API for JSON import
Starter Code
// bookmarkManager.js
class BookmarkManager {
constructor(container) {
this.container = container;
this.bookmarks = this.loadBookmarks();
this.folders = this.loadFolders();
this.viewMode = 'grid'; // 'grid' or 'list'
this.activeFilter = { folder: 'All', tags: [], search: '' };
this.init();
}
init() {
this.renderUI();
this.renderFolders();
this.renderBookmarks();
this.setupDragAndDrop();
}
loadBookmarks() {
return JSON.parse(localStorage.getItem('bookmarks') || '[]');
}
loadFolders() {
return JSON.parse(localStorage.getItem('bookmarkFolders') || '["General", "Work", "Learning", "Entertainment"]');
}
saveBookmarks() {
localStorage.setItem('bookmarks', JSON.stringify(this.bookmarks));
}
addBookmark(data) {
const bookmark = {
id: Date.now().toString(36) + Math.random().toString(36).slice(2),
url: data.url,
title: data.title || this.extractTitle(data.url),
description: data.description || '',
tags: data.tags || [],
folder: data.folder || 'General',
favicon: this.getFavicon(data.url),
createdAt: new Date().toISOString(),
order: this.bookmarks.length
};
// TODO: Validate URL format
// TODO: Check for duplicate URLs
this.bookmarks.push(bookmark);
this.saveBookmarks();
this.renderBookmarks();
}
editBookmark(id, updates) {
// TODO: Find bookmark by id and update fields
const index = this.bookmarks.findIndex(b => b.id === id);
if (index !== -1) {
this.bookmarks[index] = { ...this.bookmarks[index], ...updates };
this.saveBookmarks();
this.renderBookmarks();
}
}
deleteBookmark(id) {
// TODO: Remove bookmark and re-render
this.bookmarks = this.bookmarks.filter(b => b.id !== id);
this.saveBookmarks();
this.renderBookmarks();
}
getFavicon(url) {
try {
const domain = new URL(url).origin;
return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`;
} catch {
return 'default-favicon.png';
}
}
searchBookmarks(query) {
this.activeFilter.search = query.toLowerCase();
this.renderBookmarks();
}
filterByTag(tag) {
// TODO: Toggle tag in active filters
const index = this.activeFilter.tags.indexOf(tag);
if (index === -1) this.activeFilter.tags.push(tag);
else this.activeFilter.tags.splice(index, 1);
this.renderBookmarks();
}
filterByFolder(folder) {
this.activeFilter.folder = folder;
this.renderBookmarks();
}
getFilteredBookmarks() {
return this.bookmarks.filter(bookmark => {
const matchesFolder = this.activeFilter.folder === 'All' || bookmark.folder === this.activeFilter.folder;
const matchesTags = this.activeFilter.tags.length === 0 || this.activeFilter.tags.every(tag => bookmark.tags.includes(tag));
const matchesSearch = !this.activeFilter.search ||
bookmark.title.toLowerCase().includes(this.activeFilter.search) ||
bookmark.url.toLowerCase().includes(this.activeFilter.search) ||
bookmark.tags.some(tag => tag.toLowerCase().includes(this.activeFilter.search));
return matchesFolder && matchesTags && matchesSearch;
});
}
setupDragAndDrop() {
// TODO: Implement drag-and-drop reordering
// Use HTML5 Drag and Drop API
}
toggleView() {
this.viewMode = this.viewMode === 'grid' ? 'list' : 'grid';
this.renderBookmarks();
}
exportBookmarks() {
const data = JSON.stringify(this.bookmarks, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
// TODO: Create download link and trigger click
const a = document.createElement('a');
a.href = url;
a.download = 'bookmarks-export.json';
a.click();
URL.revokeObjectURL(url);
}
importBookmarks(file) {
// TODO: Read JSON file and merge/replace bookmarks
const reader = new FileReader();
reader.onload = (e) => {
try {
const imported = JSON.parse(e.target.result);
this.bookmarks = [...this.bookmarks, ...imported];
this.saveBookmarks();
this.renderBookmarks();
} catch (err) {
alert('Invalid bookmark file format');
}
};
reader.readAsText(file);
}
renderUI() {
// TODO: Create search bar, view toggle, folder sidebar, main content area
}
renderFolders() {
// TODO: Render folder list in sidebar
}
renderBookmarks() {
// TODO: Render filtered bookmarks in grid or list format
}
getAllTags() {
const tags = new Set();
this.bookmarks.forEach(b => b.tags.forEach(t => tags.add(t)));
return [...tags];
}
}
const managerContainer = document.getElementById('bookmark-app');
const manager = new BookmarkManager(managerContainer);
// Expected Behavior:
// User adds: URL: "https://developer.mozilla.org", Title: "MDN Web Docs"
// Tags: ["javascript", "docs"], Folder: "Learning"
// → Bookmark card appears with MDN favicon, title, and tags
// User searches "javascript" → shows all bookmarks with matching title/tag/URL
// User clicks "Learning" folder → filters to bookmarks in that folder
// User clicks tag "docs" → further filters by tag
// User drags bookmark card → reorders within the view
// User clicks "Export" → downloads bookmarks-export.json
// User toggles view → switches between grid cards and list rows
// User clicks bookmark → opens URL in new tab
Assignment 10: Pomodoro Timer
Description
Build a Pomodoro Technique timer application with configurable work/break intervals, session tracking, task management, statistics, and notification support. The Pomodoro Technique alternates focused work sessions (25 min) with short breaks (5 min), with a longer break (15 min) after every 4 sessions.
Requirements / Features
- Configurable timer durations (work: 25min, short break: 5min, long break: 15min)
- Automatic transition between work and break sessions
- Long break after every 4 completed pomodoros
- Task list integration — assign pomodoros to tasks
- Session history with daily/weekly statistics
- Desktop notifications when timer completes (using Notification API)
- Ambient sounds option (ticking, rain, white noise)
- Daily goal setting (e.g., "Complete 8 pomodoros today")
- Visual progress ring and session counter
- Keyboard shortcuts (Space to start/pause, R to reset)
- Persist settings and history in localStorage
Hints
- Use the Notification API:
Notification.requestPermission() and new Notification() - Track state: 'work', 'shortBreak', 'longBreak', with a pomodoro counter
- Use Web Audio API
AudioContext for ambient sounds - Store daily stats with date as key for easy lookup
- Use
document.title to show remaining time in browser tab
Starter Code
// pomodoroTimer.js
class PomodoroTimer {
constructor(container) {
this.container = container;
this.settings = this.loadSettings();
this.state = 'idle'; // 'idle', 'work', 'shortBreak', 'longBreak'
this.timeRemaining = this.settings.workDuration * 60;
this.intervalId = null;
this.pomodoroCount = 0;
this.dailyCount = 0;
this.tasks = this.loadTasks();
this.activeTaskId = null;
this.history = this.loadHistory();
this.isRunning = false;
this.init();
}
getDefaultSettings() {
return {
workDuration: 25, // minutes
shortBreakDuration: 5,
longBreakDuration: 15,
longBreakInterval: 4, // pomodoros before long break
dailyGoal: 8,
autoStartBreaks: true,
autoStartPomodoros: false,
soundEnabled: true,
notificationsEnabled: true
};
}
loadSettings() {
const saved = localStorage.getItem('pomodoroSettings');
return saved ? JSON.parse(saved) : this.getDefaultSettings();
}
saveSettings() {
localStorage.setItem('pomodoroSettings', JSON.stringify(this.settings));
}
init() {
this.requestNotificationPermission();
this.renderUI();
this.renderTasks();
this.updateDisplay();
this.setupKeyboardShortcuts();
this.loadTodayStats();
}
requestNotificationPermission() {
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
}
start() {
if (this.state === 'idle') {
this.state = 'work';
this.timeRemaining = this.settings.workDuration * 60;
}
this.isRunning = true;
this.intervalId = setInterval(() => this.tick(), 1000);
}
pause() {
this.isRunning = false;
clearInterval(this.intervalId);
}
reset() {
this.pause();
this.state = 'idle';
this.timeRemaining = this.settings.workDuration * 60;
this.updateDisplay();
}
tick() {
this.timeRemaining--;
this.updateDisplay();
this.updateBrowserTitle();
if (this.timeRemaining <= 0) {
this.sessionComplete();
}
}
sessionComplete() {
this.pause();
this.playCompletionSound();
this.sendNotification();
if (this.state === 'work') {
this.pomodoroCount++;
this.dailyCount++;
this.recordSession();
this.updateTaskPomodoros();
// Determine next state
if (this.pomodoroCount % this.settings.longBreakInterval === 0) {
this.transitionTo('longBreak');
} else {
this.transitionTo('shortBreak');
}
} else {
// Break complete, start next work session
this.transitionTo('work');
}
}
transitionTo(newState) {
this.state = newState;
switch(newState) {
case 'work':
this.timeRemaining = this.settings.workDuration * 60;
if (this.settings.autoStartPomodoros) this.start();
break;
case 'shortBreak':
this.timeRemaining = this.settings.shortBreakDuration * 60;
if (this.settings.autoStartBreaks) this.start();
break;
case 'longBreak':
this.timeRemaining = this.settings.longBreakDuration * 60;
if (this.settings.autoStartBreaks) this.start();
break;
}
this.updateDisplay();
}
updateDisplay() {
const minutes = Math.floor(this.timeRemaining / 60);
const seconds = this.timeRemaining % 60;
const display = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
// TODO: Update timer display element
// TODO: Update progress ring
// TODO: Update session state label
// TODO: Update daily progress (e.g., "4/8 pomodoros")
}
updateBrowserTitle() {
const minutes = Math.floor(this.timeRemaining / 60);
const seconds = this.timeRemaining % 60;
const timeStr = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
const stateLabel = this.state === 'work' ? '🍅' : '☕';
document.title = `${timeStr} ${stateLabel} Pomodoro Timer`;
}
sendNotification() {
if (!this.settings.notificationsEnabled) return;
if (Notification.permission !== 'granted') return;
const messages = {
work: '🍅 Work session complete! Time for a break.',
shortBreak: '☕ Break over! Ready to focus again?',
longBreak: '🎉 Long break over! You are doing great!'
};
new Notification('Pomodoro Timer', {
body: messages[this.state],
icon: 'pomodoro-icon.png'
});
}
playCompletionSound() {
if (!this.settings.soundEnabled) return;
// TODO: Play completion chime
const audio = new Audio('complete.mp3');
audio.play().catch(() => {});
}
// ---- Task Management ----
loadTasks() {
return JSON.parse(localStorage.getItem('pomodoroTasks') || '[]');
}
saveTasks() {
localStorage.setItem('pomodoroTasks', JSON.stringify(this.tasks));
}
addTask(title, estimatedPomodoros = 1) {
const task = {
id: Date.now().toString(36),
title,
estimatedPomodoros,
completedPomodoros: 0,
done: false,
createdAt: new Date().toISOString()
};
this.tasks.push(task);
this.saveTasks();
this.renderTasks();
}
setActiveTask(taskId) {
this.activeTaskId = taskId;
// TODO: Highlight active task in UI
}
updateTaskPomodoros() {
if (!this.activeTaskId) return;
const task = this.tasks.find(t => t.id === this.activeTaskId);
if (task) {
task.completedPomodoros++;
if (task.completedPomodoros >= task.estimatedPomodoros) {
task.done = true;
}
this.saveTasks();
this.renderTasks();
}
}
renderTasks() {
// TODO: Render task list with progress indicators
}
// ---- History & Statistics ----
loadHistory() {
return JSON.parse(localStorage.getItem('pomodoroHistory') || '{}');
}
recordSession() {
const today = new Date().toISOString().split('T')[0];
if (!this.history[today]) {
this.history[today] = { sessions: 0, totalMinutes: 0 };
}
this.history[today].sessions++;
this.history[today].totalMinutes += this.settings.workDuration;
localStorage.setItem('pomodoroHistory', JSON.stringify(this.history));
}
loadTodayStats() {
const today = new Date().toISOString().split('T')[0];
const todayData = this.history[today];
this.dailyCount = todayData ? todayData.sessions : 0;
}
getWeeklyStats() {
// TODO: Calculate stats for the last 7 days
const stats = [];
for (let i = 6; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const key = date.toISOString().split('T')[0];
stats.push({
date: key,
sessions: this.history[key]?.sessions || 0,
minutes: this.history[key]?.totalMinutes || 0
});
}
return stats;
}
// ---- Keyboard Shortcuts ----
setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT') return; // Don't interfere with input fields
switch(e.code) {
case 'Space':
e.preventDefault();
this.isRunning ? this.pause() : this.start();
break;
case 'KeyR':
this.reset();
break;
case 'KeyS':
// Skip to next session
this.sessionComplete();
break;
}
});
}
renderUI() {
// TODO: Create full UI with timer, controls, tasks, and stats panels
}
}
const pomodoroContainer = document.getElementById('pomodoro-app');
const pomodoro = new PomodoroTimer(pomodoroContainer);
// Expected Behavior:
// App loads → Timer shows "25:00" with "Start" button and state "Work"
// User presses Space → timer starts counting down: 24:59, 24:58...
// Browser tab title: "24:58 🍅 Pomodoro Timer"
// Timer reaches 00:00 → chime plays, notification: "Work session complete!"
// Auto-transitions to short break: "05:00" with state "Short Break"
// After 4 work sessions → long break: "15:00" with state "Long Break"
// User adds task "Write documentation" (est. 3 pomodoros)
// Sets it as active → after each work session, task progress updates: "1/3", "2/3", "3/3 ✓"
// Daily progress: "4/8 pomodoros completed" with progress bar
// Weekly stats: Bar chart showing sessions per day for the last 7 days
// Keyboard: Space = Start/Pause, R = Reset, S = Skip
Bonus Tips for All Assignments
Best Practices to Follow
- Separation of Concerns — Keep HTML structure, CSS styling, and JavaScript logic in separate files
- DRY Principle — Don't Repeat Yourself; extract reusable functions
- Error Handling — Always validate inputs and handle edge cases gracefully
- Accessibility — Add ARIA labels, keyboard navigation, and focus management
- Responsive Design — Test on mobile, tablet, and desktop viewports
- Code Comments — Write meaningful comments explaining "why", not just "what"
- Version Control — Use Git to track your progress and experiment with branches
Submission Checklist
- [ ] All core features implemented and working
- [ ] No console errors in browser DevTools
- [ ] Responsive on mobile devices
- [ ] Data persists after page refresh (where applicable)
- [ ] Edge cases handled (empty inputs, invalid data, boundary conditions)
- [ ] Code is well-organized with consistent naming conventions
- [ ] README.md with setup instructions and feature list
Grading Criteria
| Criteria | Weightage |
|---|
| Functionality (all features work) | 40% |
| Code Quality (clean, readable, DRY) | 25% |
| UI/UX (user-friendly, responsive) | 20% |
| Error Handling & Edge Cases | 15% |
Challenge yourself! Once you complete all 10 assignments, try combining them. For example, integrate the Pomodoro Timer with the Bookmark Manager to track study resources, or connect the Expense Tracker with the Shopping Cart for a full e-commerce experience.
Happy Coding! 🚀