Part of Chrome Extension Development series

Building a Chrome Extension with Modern JavaScript

Complete guide to building the F1 Schedule Chrome Extension with real-time Formula 1 data, modern JavaScript, and professional UI design. From concept to Chrome Web Store publication.

January 5, 2025
7 min read
By Leo Pham
chrome-extensionjavascriptf1web-developmentapi
Share this post:

Chrome Extension Development Photo by Fatos Bytyqi on Unsplash

🏎️ Building a Chrome Extension with Modern JavaScript

🚀 Project Showcase: Learn how I built the F1 Schedule Chrome Extension that brings real-time Formula 1 race information directly to your browser. Complete with API integration, modern UI, and professional deployment.

Creating browser extensions is an excellent way to solve real-world problems while learning new technologies. Today, I'll walk you through the complete journey of building the F1 Schedule Chrome Extension - from initial concept to Chrome Web Store publication.

📋 Project Overview

🎯 The Problem

As a Formula 1 fan and developer, I found myself constantly switching between multiple websites to check:

🔍 Pain Points:

  • • **Multiple tabs** for race schedules
  • • **Time zone confusion** for race times
  • • **Scattered information** across different sites
  • • **No quick access** to current standings

🎯 The Solution

✅ F1 Schedule Extension Features:

🛠️ Technical Implementation

Project Structure

f1-schedule-extension/
├── manifest.json          # Extension configuration
├── popup.html             # Main UI
├── popup.js              # Logic and API calls
├── styles.css            # Styling
├── background.js         # Background scripts
└── icons/               # Extension icons
    ├── icon16.png
    ├── icon48.png
    └── icon128.png

Manifest Configuration

The manifest.json file defines the extension's capabilities:

{
  "manifest_version": 3,
  "name": "F1 Schedule",
  "version": "1.0.0",
  "description": "Get latest F1 race events, standings, and news",
  "permissions": [
    "storage",
    "activeTab"
  ],
  "host_permissions": [
    "https://api.formula1.com/*",
    "https://ergast.com/*"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_title": "F1 Schedule"
  },
  "background": {
    "service_worker": "background.js"
  },
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

API Integration

I used the Ergast API for F1 data, which provides comprehensive racing information:

class F1API {
  constructor() {
    this.baseURL = 'https://ergast.com/api/f1';
    this.currentSeason = new Date().getFullYear();
  }

  async fetchRaceSchedule() {
    try {
      const response = await fetch(
        `${this.baseURL}/${this.currentSeason}.json`
      );
      const data = await response.json();
      return data.MRData.RaceTable.Races;
    } catch (error) {
      console.error('Error fetching race schedule:', error);
      return [];
    }
  }

  async fetchDriverStandings() {
    try {
      const response = await fetch(
        `${this.baseURL}/${this.currentSeason}/driverStandings.json`
      );
      const data = await response.json();
      return data.MRData.StandingsTable.StandingsLists[0].DriverStandings;
    } catch (error) {
      console.error('Error fetching driver standings:', error);
      return [];
    }
  }

  async fetchConstructorStandings() {
    try {
      const response = await fetch(
        `${this.baseURL}/${this.currentSeason}/constructorStandings.json`
      );
      const data = await response.json();
      return data.MRData.StandingsTable.StandingsLists[0].ConstructorStandings;
    } catch (error) {
      console.error('Error fetching constructor standings:', error);
      return [];
    }
  }
}

UI Components

The popup interface uses modern JavaScript and CSS for a clean, responsive design:

class F1Popup {
  constructor() {
    this.api = new F1API();
    this.currentView = 'schedule';
    this.init();
  }

  async init() {
    this.setupEventListeners();
    await this.loadData();
  }

  setupEventListeners() {
    document.querySelectorAll('.tab-button').forEach(button => {
      button.addEventListener('click', (e) => {
        this.switchTab(e.target.dataset.tab);
      });
    });

    document.getElementById('refresh-button').addEventListener('click', () => {
      this.loadData();
    });
  }

  async loadData() {
    this.showLoading();
    
    try {
      switch (this.currentView) {
        case 'schedule':
          await this.loadRaceSchedule();
          break;
        case 'drivers':
          await this.loadDriverStandings();
          break;
        case 'constructors':
          await this.loadConstructorStandings();
          break;
      }
    } catch (error) {
      this.showError('Failed to load data');
    } finally {
      this.hideLoading();
    }
  }

  async loadRaceSchedule() {
    const races = await this.api.fetchRaceSchedule();
    const container = document.getElementById('schedule-container');
    
    container.innerHTML = races.map(race => `
      <div class="race-card">
        <div class="race-header">
          <h3>${race.raceName}</h3>
          <span class="race-round">Round ${race.round}</span>
        </div>
        <div class="race-details">
          <p><strong>Circuit:</strong> ${race.Circuit.circuitName}</p>
          <p><strong>Location:</strong> ${race.Circuit.Location.locality}, ${race.Circuit.Location.country}</p>
          <p><strong>Date:</strong> ${this.formatDate(race.date, race.time)}</p>
        </div>
      </div>
    `).join('');
  }

  formatDate(date, time) {
    const raceDate = new Date(`${date}T${time}`);
    const options = {
      weekday: 'short',
      month: 'short',
      day: 'numeric',
      hour: '2-digit',
      minute: '2-digit',
      timeZoneName: 'short'
    };
    return raceDate.toLocaleDateString('en-US', options);
  }

  switchTab(tabName) {
    this.currentView = tabName;
    
    // Update tab buttons
    document.querySelectorAll('.tab-button').forEach(button => {
      button.classList.toggle('active', button.dataset.tab === tabName);
    });
    
    // Update content containers
    document.querySelectorAll('.tab-content').forEach(content => {
      content.classList.toggle('active', content.id === `${tabName}-container`);
    });
    
    this.loadData();
  }
}

// Initialize the popup when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
  new F1Popup();
});

🎨 Styling and UX

The extension uses a modern, F1-inspired design with:

:root {
  --f1-red: #e10600;
  --f1-black: #15151e;
  --f1-white: #ffffff;
  --f1-gray: #949498;
}

.popup-container {
  width: 400px;
  min-height: 500px;
  background: var(--f1-white);
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

.header {
  background: linear-gradient(135deg, var(--f1-red), #ff3e30);
  color: var(--f1-white);
  padding: 16px;
  text-align: center;
}

.tabs {
  display: flex;
  background: var(--f1-black);
}

.tab-button {
  flex: 1;
  padding: 12px;
  background: transparent;
  color: var(--f1-gray);
  border: none;
  cursor: pointer;
  transition: all 0.3s ease;
}

.tab-button.active {
  background: var(--f1-red);
  color: var(--f1-white);
}

.race-card {
  border: 1px solid #eee;
  border-radius: 8px;
  padding: 16px;
  margin: 8px 0;
  transition: transform 0.2s ease;
}

.race-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

🚀 Key Features Implemented

1. Caching Strategy

async fetchWithCache(url, cacheKey, ttl = 3600000) { // 1 hour TTL
  const cached = await this.getFromStorage(cacheKey);
  
  if (cached && Date.now() - cached.timestamp < ttl) {
    return cached.data;
  }
  
  const data = await fetch(url).then(r => r.json());
  await this.saveToStorage(cacheKey, {
    data,
    timestamp: Date.now()
  });
  
  return data;
}

2. Error Handling

async safeApiCall(apiFunction, fallbackData = []) {
  try {
    return await apiFunction();
  } catch (error) {
    console.error('API call failed:', error);
    this.showNotification('Using cached data', 'warning');
    return fallbackData;
  }
}

3. Local Storage Management

async saveToStorage(key, data) {
  return chrome.storage.local.set({ [key]: data });
}

async getFromStorage(key) {
  const result = await chrome.storage.local.get(key);
  return result[key];
}

📦 Publishing to Chrome Web Store

The publishing process involves:

  1. Package the extension
  2. Create developer account
  3. Upload and configure
  4. Submit for review
# Create a zip file with all extension files
zip -r f1-schedule-extension.zip . -x "*.git*" "node_modules/*" "*.md"

🔮 Future Enhancements

🚀 Roadmap Features:

**🔔 Race Notifications** - Alerts for upcoming sessions
**📊 Lap Time Analysis** - Detailed timing data
**🏁 Live Race Updates** - Real-time position tracking
**⚙️ Customizable Interface** - User preferences and themes

💡 Key Learnings & Best Practices

Aspect Learning Implementation
Manifest V3 New service worker model Background scripts optimization
API Management Rate limiting strategies Smart caching with TTL
UX Design Quick, focused interactions Minimal popup interface
Testing Cross-browser compatibility Chrome, Edge, Firefox support

⚡ Pro Tips:

  1. **Cache API responses** to reduce load times
  2. **Handle offline scenarios** gracefully
  3. **Optimize bundle size** for faster loading
  4. **Test across browsers** early and often

🎯 Try It Yourself

📚 Resources & Links:

• **GitHub Repository**: [F1 Schedule Extension](https://github.com/leoodz/f1schedule)

• **Chrome Web Store**: [Install Extension](https://chrome.google.com/webstore)

• **Documentation**: [Chrome Extension API](https://developer.chrome.com/docs/extensions/)

• **F1 API**: [Ergast Developer API](http://ergast.com/mrd/)

The extension demonstrates how modern web technologies can create useful, performance-focused browser tools that solve real problems for users.


💬 Join the Discussion:

Have you built any Chrome extensions? What challenges did you face? Share your experiences and let's learn together!

Follow me for more web development tutorials and project breakdowns!


This tutorial demonstrates real-world Chrome extension development with modern JavaScript. All code is open source and available for learning and contribution.

Last updated: January 5, 2025

Comments