Skip to content

JavaScript Browser Usage

Complete guide for using the Danish Parliament API in web browsers with CORS handling, modern JavaScript, and interactive examples.

Overview

The Danish Parliament API supports CORS (Cross-Origin Resource Sharing), making it directly accessible from web browsers without a backend proxy. This enables powerful client-side applications for parliamentary data visualization and analysis.

Browser Compatibility

  • Modern browsers: Chrome 42+, Firefox 39+, Safari 10.1+, Edge 14+
  • Native fetch API: No external dependencies required
  • ES6+ features: Async/await, classes, modules
  • CORS enabled: Direct API access from any domain

Basic Browser Setup

1. HTML Structure

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Danish Parliament API Browser Demo</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
        }
        .loading {
            color: #666;
            font-style: italic;
        }
        .error {
            color: #d32f2f;
            background: #ffebee;
            padding: 10px;
            border-radius: 4px;
            margin: 10px 0;
        }
        .success {
            color: #388e3c;
            background: #e8f5e8;
            padding: 10px;
            border-radius: 4px;
            margin: 10px 0;
        }
        .case-card {
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 15px;
            margin: 10px 0;
            background: #fafafa;
        }
        .case-title {
            font-weight: bold;
            color: #1976d2;
            margin-bottom: 5px;
        }
        .case-meta {
            font-size: 0.9em;
            color: #666;
        }
        .controls {
            margin: 20px 0;
            padding: 20px;
            background: #f5f5f5;
            border-radius: 8px;
        }
        .controls input, .controls button {
            margin: 5px;
            padding: 8px 12px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .controls button {
            background: #1976d2;
            color: white;
            cursor: pointer;
        }
        .controls button:hover {
            background: #1565c0;
        }
        #progressBar {
            width: 100%;
            height: 20px;
            background: #f0f0f0;
            border-radius: 10px;
            overflow: hidden;
            margin: 10px 0;
        }
        #progressFill {
            height: 100%;
            background: #4caf50;
            width: 0%;
            transition: width 0.3s ease;
        }
    </style>
</head>
<body>
    <h1>Danish Parliament API Browser Demo</h1>

    <div class="controls">
        <h3>Search Parliamentary Cases</h3>
        <input type="text" id="searchTerm" placeholder="Search term (e.g., 'klima')" value="klima">
        <input type="number" id="maxResults" placeholder="Max results" value="20" min="1" max="100">
        <button onclick="searchCases()">Search Cases</button>
        <button onclick="getRecentActivity()">Get Recent Activity</button>
        <button onclick="clearResults()">Clear Results</button>
    </div>

    <div id="status" class="loading" style="display: none;">Loading...</div>
    <div id="progressBar" style="display: none;">
        <div id="progressFill"></div>
    </div>
    <div id="results"></div>

    <script type="module">
        // Import the API client (assuming it's in the same directory)
        import { DanishParliamentAPI } from './danish-parliament-api.js';

        // Initialize the API client
        const api = new DanishParliamentAPI({
            requestDelay: 200 // Be extra respectful in browsers
        });

        // Make functions globally available
        window.api = api;
        window.searchCases = searchCases;
        window.getRecentActivity = getRecentActivity;
        window.clearResults = clearResults;

        // Search function
        async function searchCases() {
            const searchTerm = document.getElementById('searchTerm').value.trim();
            const maxResults = parseInt(document.getElementById('maxResults').value) || 20;

            if (!searchTerm) {
                showError('Please enter a search term');
                return;
            }

            showStatus('Searching for cases...');
            showProgress(0);

            try {
                // Search for cases
                const response = await api.getCases({
                    filter: `substringof('${searchTerm}', titel)`,
                    top: Math.min(maxResults, 100),
                    orderby: 'opdateringsdato desc'
                });

                showProgress(100);
                displayCases(response.value, `Search results for "${searchTerm}"`);
                showSuccess(`Found ${response.value.length} cases`);

            } catch (error) {
                showError(`Search failed: ${error.message}`);
                hideProgress();
            }
        }

        // Get recent activity
        async function getRecentActivity() {
            showStatus('Fetching recent parliamentary activity...');
            showProgress(0);

            try {
                const response = await api.getRecentChanges('Sag', 24); // Last 24 hours

                showProgress(100);
                displayCases(response.value, 'Recent Parliamentary Activity (Last 24 hours)');
                showSuccess(`Found ${response.value.length} recently updated cases`);

            } catch (error) {
                showError(`Failed to fetch recent activity: ${error.message}`);
                hideProgress();
            }
        }

        // Display cases in the UI
        function displayCases(cases, title) {
            const resultsDiv = document.getElementById('results');

            if (cases.length === 0) {
                resultsDiv.innerHTML = `<div class="error">No cases found</div>`;
                return;
            }

            let html = `<h2>${title}</h2>`;

            cases.forEach(case => {
                const updatedDate = new Date(case.opdateringsdato).toLocaleDateString('da-DK');
                html += `
                    <div class="case-card">
                        <div class="case-title">${case.titel}</div>
                        <div class="case-meta">
                            ID: ${case.id} | 
                            Updated: ${updatedDate} | 
                            Type: ${case.typeid}
                        </div>
                    </div>
                `;
            });

            resultsDiv.innerHTML = html;
            hideStatus();
        }

        // UI helper functions
        function showStatus(message) {
            const statusDiv = document.getElementById('status');
            statusDiv.textContent = message;
            statusDiv.style.display = 'block';
            statusDiv.className = 'loading';
        }

        function showSuccess(message) {
            const statusDiv = document.getElementById('status');
            statusDiv.textContent = message;
            statusDiv.style.display = 'block';
            statusDiv.className = 'success';
            setTimeout(() => hideStatus(), 3000);
        }

        function showError(message) {
            const statusDiv = document.getElementById('status');
            statusDiv.textContent = message;
            statusDiv.style.display = 'block';
            statusDiv.className = 'error';
        }

        function hideStatus() {
            document.getElementById('status').style.display = 'none';
        }

        function showProgress(percentage) {
            const progressBar = document.getElementById('progressBar');
            const progressFill = document.getElementById('progressFill');

            progressBar.style.display = 'block';
            progressFill.style.width = percentage + '%';
        }

        function hideProgress() {
            document.getElementById('progressBar').style.display = 'none';
        }

        function clearResults() {
            document.getElementById('results').innerHTML = '';
            hideStatus();
            hideProgress();
        }

        // Initialize with a sample search on page load
        window.addEventListener('load', () => {
            console.log('Danish Parliament API Browser Demo loaded');
            console.log('Try searching for "klima" to see climate-related legislation');
        });
    </script>
</body>
</html>

2. Advanced Interactive Dashboard

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Parliamentary Dashboard</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 20px;
            background: #f5f5f5;
        }
        .dashboard {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
            gap: 20px;
            max-width: 1400px;
            margin: 0 auto;
        }
        .widget {
            background: white;
            border-radius: 12px;
            padding: 20px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .widget h3 {
            margin: 0 0 15px 0;
            color: #333;
            border-bottom: 2px solid #e0e0e0;
            padding-bottom: 10px;
        }
        .stat-grid {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 15px;
            margin-bottom: 20px;
        }
        .stat-item {
            text-align: center;
            padding: 15px;
            background: #f8f9fa;
            border-radius: 8px;
        }
        .stat-number {
            font-size: 2em;
            font-weight: bold;
            color: #1976d2;
        }
        .stat-label {
            color: #666;
            font-size: 0.9em;
        }
        .chart-container {
            position: relative;
            height: 300px;
            margin-top: 20px;
        }
        .loading-spinner {
            border: 4px solid #f3f3f3;
            border-top: 4px solid #1976d2;
            border-radius: 50%;
            width: 40px;
            height: 40px;
            animation: spin 1s linear infinite;
            margin: 20px auto;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
    </style>
</head>
<body>
    <h1>Danish Parliament Live Dashboard</h1>

    <div class="dashboard">
        <!-- Statistics Widget -->
        <div class="widget">
            <h3>=Ê Parliamentary Statistics</h3>
            <div id="statsLoader" class="loading-spinner"></div>
            <div id="statsContent" style="display: none;">
                <div class="stat-grid">
                    <div class="stat-item">
                        <div class="stat-number" id="totalCases">-</div>
                        <div class="stat-label">Total Cases</div>
                    </div>
                    <div class="stat-item">
                        <div class="stat-number" id="totalActors">-</div>
                        <div class="stat-label">Total Actors</div>
                    </div>
                    <div class="stat-item">
                        <div class="stat-number" id="recentUpdates">-</div>
                        <div class="stat-label">Updates Today</div>
                    </div>
                    <div class="stat-item">
                        <div class="stat-number" id="apiResponseTime">-</div>
                        <div class="stat-label">API Response (ms)</div>
                    </div>
                </div>
            </div>
        </div>

        <!-- Recent Activity Widget -->
        <div class="widget">
            <h3>¡ Recent Activity</h3>
            <div id="recentLoader" class="loading-spinner"></div>
            <div id="recentActivity" style="display: none;"></div>
        </div>

        <!-- Topic Analysis Widget -->
        <div class="widget">
            <h3><÷ Topic Analysis</h3>
            <div id="topicsLoader" class="loading-spinner"></div>
            <div class="chart-container">
                <canvas id="topicsChart" style="display: none;"></canvas>
            </div>
        </div>

        <!-- Real-time Monitor Widget -->
        <div class="widget">
            <h3>=4 Live Monitor</h3>
            <button id="startMonitoring" onclick="toggleMonitoring()">Start Monitoring</button>
            <div id="monitorStatus" style="margin-top: 10px;"></div>
            <div id="liveUpdates" style="max-height: 300px; overflow-y: auto; margin-top: 15px;"></div>
        </div>
    </div>

    <script type="module">
        import { DanishParliamentAPI } from './danish-parliament-api.js';

        const api = new DanishParliamentAPI({ requestDelay: 300 });
        let monitoringInterval = null;
        let isMonitoring = false;

        // Initialize dashboard
        async function initializeDashboard() {
            await loadStatistics();
            await loadRecentActivity();
            await loadTopicAnalysis();
        }

        // Load basic statistics
        async function loadStatistics() {
            try {
                const startTime = Date.now();

                // Get counts for major entities
                const [caseCount, actorCount, recentChanges] = await Promise.all([
                    api.getEntityCount('Sag'),
                    api.getEntityCount('Aktør'),
                    api.getRecentChanges('Sag', 24)
                ]);

                const responseTime = Date.now() - startTime;

                // Update UI
                document.getElementById('totalCases').textContent = caseCount.toLocaleString();
                document.getElementById('totalActors').textContent = actorCount.toLocaleString();
                document.getElementById('recentUpdates').textContent = recentChanges.value.length;
                document.getElementById('apiResponseTime').textContent = responseTime;

                // Show content, hide loader
                document.getElementById('statsLoader').style.display = 'none';
                document.getElementById('statsContent').style.display = 'block';

            } catch (error) {
                console.error('Failed to load statistics:', error);
                document.getElementById('statsLoader').innerHTML = '<div style="color: red;">Failed to load</div>';
            }
        }

        // Load recent activity
        async function loadRecentActivity() {
            try {
                const recent = await api.getRecentChanges('Sag', 6); // Last 6 hours
                const activityDiv = document.getElementById('recentActivity');

                if (recent.value.length === 0) {
                    activityDiv.innerHTML = '<p>No recent activity</p>';
                } else {
                    let html = '';
                    recent.value.slice(0, 5).forEach(case => {
                        const timeAgo = getTimeAgo(new Date(case.opdateringsdato));
                        html += `
                            <div style="padding: 10px; margin: 5px 0; background: #f8f9fa; border-radius: 6px;">
                                <strong>${case.titel.substring(0, 60)}${case.titel.length > 60 ? '...' : ''}</strong><br>
                                <small style="color: #666;">Updated ${timeAgo}</small>
                            </div>
                        `;
                    });
                    activityDiv.innerHTML = html;
                }

                document.getElementById('recentLoader').style.display = 'none';
                activityDiv.style.display = 'block';

            } catch (error) {
                console.error('Failed to load recent activity:', error);
                document.getElementById('recentLoader').innerHTML = '<div style="color: red;">Failed to load</div>';
            }
        }

        // Load topic analysis
        async function loadTopicAnalysis() {
            try {
                // Search for common topics
                const topics = ['klima', 'miljø', 'økonomi', 'sundhed', 'uddannelse'];
                const results = await Promise.all(
                    topics.map(async topic => {
                        const response = await api.getCases({
                            filter: `substringof('${topic}', titel)`,
                            top: 1
                        });
                        // Get count from a separate query
                        const countResponse = await api.request('Sag', {
                            '$filter': `substringof('${topic}', titel)`,
                            '$inlinecount': 'allpages',
                            '$top': 1
                        });
                        return {
                            topic: topic,
                            count: parseInt(countResponse['odata.count'] || 0)
                        };
                    })
                );

                // Create chart
                const ctx = document.getElementById('topicsChart').getContext('2d');
                new Chart(ctx, {
                    type: 'bar',
                    data: {
                        labels: results.map(r => r.topic.charAt(0).toUpperCase() + r.topic.slice(1)),
                        datasets: [{
                            label: 'Number of Cases',
                            data: results.map(r => r.count),
                            backgroundColor: [
                                '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF'
                            ]
                        }]
                    },
                    options: {
                        responsive: true,
                        maintainAspectRatio: false,
                        plugins: {
                            title: {
                                display: true,
                                text: 'Cases by Topic'
                            }
                        }
                    }
                });

                document.getElementById('topicsLoader').style.display = 'none';
                document.getElementById('topicsChart').style.display = 'block';

            } catch (error) {
                console.error('Failed to load topic analysis:', error);
                document.getElementById('topicsLoader').innerHTML = '<div style="color: red;">Failed to load</div>';
            }
        }

        // Real-time monitoring
        window.toggleMonitoring = function() {
            if (isMonitoring) {
                stopMonitoring();
            } else {
                startMonitoring();
            }
        };

        function startMonitoring() {
            isMonitoring = true;
            document.getElementById('startMonitoring').textContent = 'Stop Monitoring';
            document.getElementById('monitorStatus').innerHTML = '<span style="color: green;">=â Monitoring active</span>';

            // Check for updates every 30 seconds
            monitoringInterval = setInterval(checkForUpdates, 30000);
            checkForUpdates(); // Initial check
        }

        function stopMonitoring() {
            isMonitoring = false;
            document.getElementById('startMonitoring').textContent = 'Start Monitoring';
            document.getElementById('monitorStatus').innerHTML = '<span style="color: #666;">ª Monitoring stopped</span>';

            if (monitoringInterval) {
                clearInterval(monitoringInterval);
                monitoringInterval = null;
            }
        }

        async function checkForUpdates() {
            try {
                // Check for updates in the last 5 minutes
                const cutoffTime = new Date();
                cutoffTime.setMinutes(cutoffTime.getMinutes() - 5);
                const isoTime = cutoffTime.toISOString().slice(0, 19);

                const updates = await api.request('Sag', {
                    '$filter': `opdateringsdato gt datetime'${isoTime}'`,
                    '$orderby': 'opdateringsdato desc',
                    '$top': 10
                });

                const updatesDiv = document.getElementById('liveUpdates');

                if (updates.value && updates.value.length > 0) {
                    let html = '';
                    updates.value.forEach(case => {
                        const timeAgo = getTimeAgo(new Date(case.opdateringsdato));
                        html += `
                            <div style="padding: 8px; margin: 3px 0; background: #e8f5e8; border-radius: 4px; border-left: 4px solid #4caf50;">
                                <strong>New Update:</strong> ${case.titel.substring(0, 50)}...<br>
                                <small style="color: #666;">${timeAgo}</small>
                            </div>
                        `;
                    });

                    updatesDiv.innerHTML = html + updatesDiv.innerHTML;

                    // Limit to 20 updates
                    const children = updatesDiv.children;
                    while (children.length > 20) {
                        updatesDiv.removeChild(children[children.length - 1]);
                    }
                } else {
                    // Add a "no updates" message occasionally
                    if (Math.random() < 0.2) { // 20% chance
                        const now = new Date().toLocaleTimeString();
                        updatesDiv.innerHTML = `
                            <div style="padding: 8px; margin: 3px 0; background: #f0f0f0; border-radius: 4px; color: #666;">
                                No new updates (checked at ${now})
                            </div>
                        ` + updatesDiv.innerHTML;
                    }
                }

            } catch (error) {
                console.error('Error checking for updates:', error);
                document.getElementById('monitorStatus').innerHTML = '<span style="color: red;">L Monitor error</span>';
            }
        }

        // Utility function to calculate time ago
        function getTimeAgo(date) {
            const now = new Date();
            const diffInSeconds = Math.floor((now - date) / 1000);

            if (diffInSeconds < 60) return `${diffInSeconds} seconds ago`;
            if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
            if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
            return `${Math.floor(diffInSeconds / 86400)} days ago`;
        }

        // Initialize dashboard when page loads
        window.addEventListener('load', () => {
            console.log('Parliamentary Dashboard initializing...');
            initializeDashboard();
        });

    </script>
</body>
</html>

CORS Configuration

The Danish Parliament API correctly supports CORS:

// These headers are returned by the API:
// Access-Control-Allow-Origin: *
// Access-Control-Allow-Methods: GET,POST,PUT,PATCH,MERGE,DELETE
// Access-Control-Allow-Headers: Content-Type

// This means you can make direct requests from any domain
const response = await fetch('https://oda.ft.dk/api/Sag?%24top=5');
const data = await response.json();

Error Handling in Browsers

// Browser-specific error handling
class BrowserAPIError extends Error {
    constructor(message, type = 'UNKNOWN') {
        super(message);
        this.name = 'BrowserAPIError';
        this.type = type;
        this.timestamp = new Date().toISOString();
    }

    // Display error in user-friendly way
    displayToUser(containerId) {
        const container = document.getElementById(containerId);
        if (container) {
            container.innerHTML = `
                <div class="error-message" style="
                    background: #ffebee; 
                    color: #c62828; 
                    padding: 15px; 
                    border-radius: 8px; 
                    margin: 10px 0;
                    border-left: 4px solid #d32f2f;
                ">
                    <strong>Error:</strong> ${this.message}<br>
                    <small>Time: ${new Date(this.timestamp).toLocaleString()}</small>
                </div>
            `;
        }
    }
}

// Enhanced error handling for browser environment
async function safeAPICall(apiCall, errorContainerId = null) {
    try {
        return await apiCall();
    } catch (error) {
        let browserError;

        if (error.name === 'TypeError' && error.message.includes('fetch')) {
            browserError = new BrowserAPIError(
                'Network connection failed. Please check your internet connection.',
                'NETWORK_ERROR'
            );
        } else if (error.name === 'AbortError') {
            browserError = new BrowserAPIError(
                'Request took too long and was cancelled.',
                'TIMEOUT_ERROR'
            );
        } else if (error.message.includes('CORS')) {
            browserError = new BrowserAPIError(
                'Cross-origin request blocked. This should not happen with the Danish Parliament API.',
                'CORS_ERROR'
            );
        } else {
            browserError = new BrowserAPIError(error.message, 'API_ERROR');
        }

        // Log for debugging
        console.error('API Error:', browserError);

        // Display to user if container provided
        if (errorContainerId) {
            browserError.displayToUser(errorContainerId);
        }

        throw browserError;
    }
}

// Usage example
async function searchWithErrorHandling() {
    await safeAPICall(
        () => api.getCases({ filter: "substringof('klima', titel)" }),
        'errorContainer'
    );
}

Progressive Web App (PWA) Example

// Service Worker for offline functionality (sw.js)
const CACHE_NAME = 'parliament-api-v1';
const CACHE_URLS = [
    '/',
    '/index.html',
    '/danish-parliament-api.js',
    'https://cdn.jsdelivr.net/npm/chart.js'
];

// Install event - cache resources
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(CACHE_URLS))
    );
});

// Fetch event - serve from cache when offline
self.addEventListener('fetch', event => {
    // Only cache GET requests to our domain or the API
    if (event.request.method === 'GET' && 
        (event.request.url.includes(location.origin) || 
         event.request.url.includes('oda.ft.dk'))) {

        event.respondWith(
            caches.match(event.request)
                .then(response => {
                    // Return cached version or fetch from network
                    return response || fetch(event.request).then(fetchResponse => {
                        // Cache successful API responses
                        if (fetchResponse.status === 200) {
                            const responseClone = fetchResponse.clone();
                            caches.open(CACHE_NAME)
                                .then(cache => cache.put(event.request, responseClone));
                        }
                        return fetchResponse;
                    });
                })
                .catch(() => {
                    // Return offline page or cached data
                    return caches.match('/offline.html');
                })
        );
    }
});
<!-- PWA Manifest (manifest.json) -->
{
    "name": "Danish Parliament Dashboard",
    "short_name": "Parliament",
    "description": "Real-time Danish Parliament data dashboard",
    "start_url": "/",
    "display": "standalone",
    "background_color": "#ffffff",
    "theme_color": "#1976d2",
    "icons": [
        {
            "src": "icon-192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "icon-512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ]
}

<!-- In HTML head -->
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#1976d2">

<!-- Register service worker -->
<script>
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js')
        .then(registration => console.log('SW registered'))
        .catch(error => console.log('SW registration failed'));
}
</script>

Performance Optimization for Browsers

1. Request Batching

class RequestBatcher {
    constructor(api, batchDelay = 100) {
        this.api = api;
        this.batchDelay = batchDelay;
        this.queue = [];
        this.timeoutId = null;
    }

    // Add request to batch
    addRequest(entity, params) {
        return new Promise((resolve, reject) => {
            this.queue.push({ entity, params, resolve, reject });

            // Start batch timer if not already running
            if (!this.timeoutId) {
                this.timeoutId = setTimeout(() => this.processBatch(), this.batchDelay);
            }
        });
    }

    // Process all queued requests
    async processBatch() {
        const currentQueue = [...this.queue];
        this.queue = [];
        this.timeoutId = null;

        // Group similar requests
        const grouped = {};
        currentQueue.forEach(req => {
            const key = `${req.entity}_${JSON.stringify(req.params)}`;
            if (!grouped[key]) {
                grouped[key] = [];
            }
            grouped[key].push(req);
        });

        // Execute unique requests
        for (const [key, requests] of Object.entries(grouped)) {
            const firstReq = requests[0];

            try {
                const result = await this.api.request(firstReq.entity, firstReq.params);

                // Resolve all similar requests with same result
                requests.forEach(req => req.resolve(result));

            } catch (error) {
                // Reject all similar requests
                requests.forEach(req => req.reject(error));
            }
        }
    }
}

// Usage
const api = new DanishParliamentAPI();
const batcher = new RequestBatcher(api);

// These will be batched together
const results = await Promise.all([
    batcher.addRequest('Sag', { '$top': 10 }),
    batcher.addRequest('Sag', { '$top': 10 }), // Duplicate - will reuse result
    batcher.addRequest('Aktør', { '$top': 5 })
]);

2. Caching Strategy

class CachedAPI {
    constructor(api, cacheDuration = 300000) { // 5 minutes default
        this.api = api;
        this.cacheDuration = cacheDuration;
        this.cache = new Map();
    }

    // Generate cache key
    getCacheKey(entity, params) {
        return `${entity}_${JSON.stringify(params)}`;
    }

    // Check if cache entry is valid
    isValidCache(entry) {
        return Date.now() - entry.timestamp < this.cacheDuration;
    }

    // Cached request
    async request(entity, params) {
        const key = this.getCacheKey(entity, params);
        const cached = this.cache.get(key);

        // Return cached result if valid
        if (cached && this.isValidCache(cached)) {
            console.log('Cache hit:', key);
            return cached.data;
        }

        // Fetch from API
        try {
            const result = await this.api.request(entity, params);

            // Cache the result
            this.cache.set(key, {
                data: result,
                timestamp: Date.now()
            });

            console.log('Cache miss:', key);
            return result;

        } catch (error) {
            // Return stale cache if available
            if (cached) {
                console.log('Using stale cache due to error:', key);
                return cached.data;
            }
            throw error;
        }
    }

    // Clear expired cache entries
    cleanupCache() {
        for (const [key, entry] of this.cache.entries()) {
            if (!this.isValidCache(entry)) {
                this.cache.delete(key);
            }
        }
    }
}

// Usage
const api = new DanishParliamentAPI();
const cachedApi = new CachedAPI(api);

// Clean up cache every 10 minutes
setInterval(() => cachedApi.cleanupCache(), 600000);

Mobile-Responsive Patterns

/* Mobile-first responsive design */
@media (max-width: 768px) {
    .dashboard {
        grid-template-columns: 1fr;
        padding: 10px;
    }

    .widget {
        padding: 15px;
    }

    .stat-grid {
        grid-template-columns: 1fr;
    }

    .controls input,
    .controls button {
        width: 100%;
        box-sizing: border-box;
        margin: 5px 0;
    }
}

/* Touch-friendly buttons */
.controls button {
    min-height: 44px; /* iOS minimum touch target */
    min-width: 44px;
}
// Mobile-specific optimizations
class MobileOptimizedAPI extends DanishParliamentAPI {
    constructor(options = {}) {
        super({
            ...options,
            // Longer timeout for mobile networks
            timeout: 45000,
            // More aggressive rate limiting
            requestDelay: 500
        });

        this.isMobile = this.detectMobile();
    }

    detectMobile() {
        return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
            .test(navigator.userAgent);
    }

    // Mobile-optimized pagination (smaller batches)
    async* paginateAll(entity, options = {}) {
        const mobileOptions = {
            ...options,
            batchSize: this.isMobile ? 20 : (options.batchSize || 100)
        };

        yield* super.paginateAll(entity, mobileOptions);
    }
}

This comprehensive browser usage guide provides everything needed to build powerful client-side applications with the Danish Parliament API, from simple demos to full-featured progressive web apps.