HTML & JS Topic

JavaScript Fetch API & AJAX 

1. Fetch API Basics

Basic GET Request

javascript

// Simple GET request
fetch('https://jsonplaceholder.typicode.com/posts')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error('Error:', error));

// With async/await
async function fetchData() {
    try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts');
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.error('Error:', error);
    }
}

Fetch Response Methods

javascript

fetch('https://api.example.com/data')
    .then(response => {
        // Check if request was successful
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        // Different ways to parse response
        // For JSON data
        return response.json();
        
        // For text data
        // return response.text();
        
        // For binary data (images, files)
        // return response.blob();
        
        // For form data
        // return response.formData();
        
        // For array buffer
        // return response.arrayBuffer();
    })
    .then(data => console.log(data))
    .catch(error => console.error('Error:', error));
Fetch with Headers & Options
javascript

// Custom headers and options
const options = {
    method: 'GET',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer your-token-here',
        'X-Custom-Header': 'custom-value'
    },
    mode: 'cors', // 'cors', 'no-cors', 'same-origin'
    cache: 'no-cache', // 'default', 'no-cache', 'reload', 'force-cache'
    credentials: 'same-origin', // 'omit', 'same-origin', 'include'
    redirect: 'follow' // 'manual', 'follow', 'error'
};

fetch('https://api.example.com/data', options)
    .then(response => response.json())
    .then(data => console.log(data));

2. HTTP Methods (GET, POST, PUT, DELETE)

GET - Retrieve Data

javascript

// Basic GET
async function getPosts() {
    try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts');
        
        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }
        
        const posts = await response.json();
        return posts;
    } catch (error) {
        console.error('Failed to fetch posts:', error);
        throw error;
    }
}

// GET with query parameters
async function searchUsers(query) {
    const url = new URL('https://api.example.com/users');
    url.searchParams.append('q', query);
    url.searchParams.append('limit', '10');
    url.searchParams.append('page', '1');
    
    const response = await fetch(url);
    return await response.json();
}

// GET with headers (API key)
async function getWeather(city) {
    const API_KEY = 'your-api-key';
    const response = await fetch(
        `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric`
    );
    
    if (!response.ok) {
        throw new Error(`Weather API error: ${response.status}`);
    }
    
    return await response.json();
}

POST - Create Data

javascript

// Basic POST with JSON
async function createPost(postData) {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(postData)
    });
    
    if (!response.ok) {
        throw new Error(`Failed to create post: ${response.status}`);
    }
    
    return await response.json();
}

// Usage
const newPost = {
    title: 'My New Post',
    body: 'This is the content of my post',
    userId: 1
};

createPost(newPost)
    .then(data => console.log('Created post:', data))
    .catch(error => console.error('Error:', error));

// POST with FormData (for file uploads)
async function uploadFile(file) {
    const formData = new FormData();
    formData.append('file', file);
    formData.append('description', 'My uploaded file');
    
    const response = await fetch('https://api.example.com/upload', {
        method: 'POST',
        body: formData
        // Note: Don't set Content-Type header for FormData
        // Fetch will set it automatically with boundary
    });
    
    return await response.json();
}

// File upload example
document.getElementById('file-input').addEventListener('change', async (e) => {
    const file = e.target.files[0];
    if (!file) return;
    
    try {
        const result = await uploadFile(file);
        console.log('Upload successful:', result);
    } catch (error) {
        console.error('Upload failed:', error);
    }
});

PUT - Update Data

javascript

// PUT to update entire resource
async function updatePost(postId, updatedData) {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
        method: 'PUT',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(updatedData)
    });
    
    if (!response.ok) {
        throw new Error(`Failed to update post: ${response.status}`);
    }
    
    return await response.json();
}

// Usage
const updatedPost = {
    id: 1,
    title: 'Updated Title',
    body: 'Updated content',
    userId: 1
};

updatePost(1, updatedPost)
    .then(data => console.log('Updated post:', data))
    .catch(error => console.error('Error:', error));

// PATCH for partial updates
async function partialUpdate(postId, partialData) {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
        method: 'PATCH',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(partialData)
    });
    
    return await response.json();
}

// Usage: Only update the title
partialUpdate(1, { title: 'Only title updated' })
    .then(data => console.log('Partially updated:', data));

DELETE - Remove Data

javascript

// DELETE request
async function deletePost(postId) {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
        method: 'DELETE'
    });
    
    if (!response.ok) {
        throw new Error(`Failed to delete post: ${response.status}`);
    }
    
    // DELETE usually returns 204 No Content or 200 with success message
    if (response.status === 204) {
        return { success: true, message: 'Post deleted successfully' };
    }
    
    return await response.json();
}

// Usage with confirmation
async function deleteWithConfirmation(postId) {
    if (!confirm('Are you sure you want to delete this post?')) {
        return;
    }
    
    try {
        const result = await deletePost(postId);
        console.log('Delete successful:', result);
        // Refresh the data or remove from UI
    } catch (error) {
        console.error('Delete failed:', error);
        alert('Failed to delete post. Please try again.');
    }
}

3. Handling JSON

Sending and Receiving JSON

javascript

// Complete JSON handling example
class ApiClient {
    constructor(baseURL, defaultHeaders = {}) {
        this.baseURL = baseURL;
        this.defaultHeaders = {
            'Content-Type': 'application/json',
            ...defaultHeaders
        };
    }
    
    async request(endpoint, options = {}) {
        const url = `${this.baseURL}${endpoint}`;
        
        const config = {
            headers: {
                ...this.defaultHeaders,
                ...options.headers
            },
            ...options
        };
        
        // Add body if present (stringify if it's not FormData)
        if (config.body && !(config.body instanceof FormData)) {
            config.body = JSON.stringify(config.body);
        }
        
        const response = await fetch(url, config);
        
        // Handle different response types
        const contentType = response.headers.get('content-type');
        let data;
        
        if (contentType && contentType.includes('application/json')) {
            data = await response.json();
        } else if (contentType && contentType.includes('text/')) {
            data = await response.text();
        } else {
            data = await response.blob();
        }
        
        // Check for errors
        if (!response.ok) {
            const error = new Error(data.message || `HTTP ${response.status}`);
            error.status = response.status;
            error.data = data;
            throw error;
        }
        
        return {
            data,
            status: response.status,
            headers: response.headers,
            ok: response.ok
        };
    }
    
    // Convenience methods
    get(endpoint, options = {}) {
        return this.request(endpoint, { method: 'GET', ...options });
    }
    
    post(endpoint, body, options = {}) {
        return this.request(endpoint, { method: 'POST', body, ...options });
    }
    
    put(endpoint, body, options = {}) {
        return this.request(endpoint, { method: 'PUT', body, ...options });
    }
    
    patch(endpoint, body, options = {}) {
        return this.request(endpoint, { method: 'PATCH', body, ...options });
    }
    
    delete(endpoint, options = {}) {
        return this.request(endpoint, { method: 'DELETE', ...options });
    }
}

// Usage
const api = new ApiClient('https://jsonplaceholder.typicode.com');

// Get all posts
api.get('/posts')
    .then(({ data }) => console.log('Posts:', data));

// Create new post
api.post('/posts', {
    title: 'New Post',
    body: 'Content here',
    userId: 1
}).then(({ data }) => console.log('Created:', data));

JSON Schema Validation

javascript

// Simple JSON validation
function validateJSON(schema, data) {
    const errors = [];
    
    // Check required fields
    if (schema.required) {
        schema.required.forEach(field => {
            if (data[field] === undefined || data[field] === null) {
                errors.push(`Missing required field: ${field}`);
            }
        });
    }
    
    // Check field types
    if (schema.properties) {
        Object.entries(schema.properties).forEach(([field, rules]) => {
            if (data[field] !== undefined) {
                if (rules.type && typeof data[field] !== rules.type) {
                    errors.push(`${field} should be ${rules.type}, got ${typeof data[field]}`);
                }
                
                if (rules.minLength && data[field].length < rules.minLength) {
                    errors.push(`${field} should be at least ${rules.minLength} characters`);
                }
                
                if (rules.maxLength && data[field].length > rules.maxLength) {
                    errors.push(`${field} should be at most ${rules.maxLength} characters`);
                }
            }
        });
    }
    
    return {
        valid: errors.length === 0,
        errors
    };
}

// Schema definition
const postSchema = {
    required: ['title', 'body', 'userId'],
    properties: {
        title: { type: 'string', minLength: 3, maxLength: 100 },
        body: { type: 'string', minLength: 10 },
        userId: { type: 'number' }
    }
};

// Usage in API call
async function createPostWithValidation(postData) {
    const validation = validateJSON(postSchema, postData);
    
    if (!validation.valid) {
        throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
    }
    
    return await api.post('/posts', postData);
}

4. API Error Handling

Comprehensive Error Handling

javascript

// Custom error classes
class ApiError extends Error {
    constructor(message, status, data = null) {
        super(message);
        this.name = 'ApiError';
        this.status = status;
        this.data = data;
        this.timestamp = new Date().toISOString();
    }
    
    toJSON() {
        return {
            name: this.name,
            message: this.message,
            status: this.status,
            data: this.data,
            timestamp: this.timestamp,
            stack: this.stack
        };
    }
}

class NetworkError extends Error {
    constructor(message, originalError = null) {
        super(message);
        this.name = 'NetworkError';
        this.originalError = originalError;
    }
}

class ValidationError extends Error {
    constructor(message, errors = []) {
        super(message);
        this.name = 'ValidationError';
        this.errors = errors;
    }
}

// Enhanced fetch with error handling
async function safeFetch(url, options = {}) {
    let response;
    
    try {
        response = await fetch(url, options);
    } catch (error) {
        // Network errors (no internet, CORS, etc.)
        throw new NetworkError(
            `Network request failed: ${error.message}`,
            error
        );
    }
    
    // Parse response
    let data;
    const contentType = response.headers.get('content-type');
    
    try {
        if (contentType && contentType.includes('application/json')) {
            data = await response.json();
        } else {
            data = await response.text();
        }
    } catch (error) {
        // Response parsing error
        throw new ApiError(
            `Failed to parse response: ${error.message}`,
            response.status,
            null
        );
    }
    
    // Check HTTP status
    if (!response.ok) {
        // Handle specific status codes
        switch (response.status) {
            case 400:
                throw new ValidationError('Bad request', data.errors || data);
            case 401:
                throw new ApiError('Unauthorized - please login', 401, data);
            case 403:
                throw new ApiError('Forbidden - insufficient permissions', 403, data);
            case 404:
                throw new ApiError('Resource not found', 404, data);
            case 429:
                throw new ApiError('Too many requests - rate limited', 429, data);
            case 500:
                throw new ApiError('Server error', 500, data);
            case 503:
                throw new ApiError('Service unavailable', 503, data);
            default:
                throw new ApiError(
                    data.message || `HTTP ${response.status}`,
                    response.status,
                    data
                );
        }
    }
    
    return {
        data,
        status: response.status,
        headers: response.headers,
        ok: response.ok
    };
}

// Usage with comprehensive error handling
async function fetchUserData(userId) {
    try {
        const result = await safeFetch(`https://api.example.com/users/${userId}`);
        return result.data;
    } catch (error) {
        // Handle different error types
        switch (error.name) {
            case 'NetworkError':
                console.error('Network issue:', error.message);
                // Show offline message to user
                showErrorMessage('Please check your internet connection');
                break;
                
            case 'ValidationError':
                console.error('Validation failed:', error.errors);
                // Show validation errors to user
                showValidationErrors(error.errors);
                break;
                
            case 'ApiError':
                console.error(`API Error ${error.status}:`, error.message);
                
                // Handle specific API errors
                if (error.status === 401) {
                    // Redirect to login
                    window.location.href = '/login';
                } else if (error.status === 429) {
                    // Show rate limit message
                    showErrorMessage('Too many requests. Please wait a moment.');
                } else {
                    // General API error
                    showErrorMessage(`API Error: ${error.message}`);
                }
                break;
                
            default:
                console.error('Unknown error:', error);
                showErrorMessage('An unexpected error occurred');
        }
        
        // Re-throw for higher level handling if needed
        throw error;
    }
}

Retry Logic with Exponential Backoff

javascript

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
    let lastError;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            const response = await fetch(url, options);
            
            if (!response.ok) {
                // Don't retry client errors (4xx) except 429
                if (response.status >= 400 && response.status < 500 && response.status !== 429) {
                    throw new Error(`Client error: ${response.status}`);
                }
                
                // Throw to trigger retry for server errors
                throw new Error(`HTTP ${response.status}`);
            }
            
            return await response.json();
            
        } catch (error) {
            lastError = error;
            
            if (attempt === maxRetries) {
                break; // No more retries
            }
            
            // Calculate delay with exponential backoff
            const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
            
            console.log(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
            
            // Wait before retrying
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
    
    throw lastError;
}

// Usage
fetchWithRetry('https://api.example.com/data', {}, 3)
    .then(data => console.log('Success after retries:', data))
    .catch(error => console.error('All retries failed:', error));

5. Practical Examples

Complete Blog API Client

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Blog API Client</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
            line-height: 1.6;
            color: #333;
            background-color: #f5f5f5;
            padding: 20px;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
        }
        
        header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 2rem;
            border-radius: 10px;
            margin-bottom: 2rem;
            text-align: center;
        }
        
        h1 {
            font-size: 2.5rem;
            margin-bottom: 0.5rem;
        }
        
        .controls {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
            gap: 20px;
            margin-bottom: 2rem;
        }
        
        .card {
            background: white;
            border-radius: 10px;
            padding: 1.5rem;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            transition: transform 0.3s ease, box-shadow 0.3s ease;
        }
        
        .card:hover {
            transform: translateY(-5px);
            box-shadow: 0 8px 15px rgba(0, 0, 0, 0.2);
        }
        
        .card h3 {
            color: #667eea;
            margin-bottom: 1rem;
            padding-bottom: 0.5rem;
            border-bottom: 2px solid #f0f0f0;
        }
        
        button {
            background: #667eea;
            color: white;
            border: none;
            padding: 0.75rem 1.5rem;
            border-radius: 5px;
            cursor: pointer;
            font-size: 1rem;
            font-weight: 600;
            transition: background 0.3s ease;
            width: 100%;
            margin-top: 10px;
        }
        
        button:hover {
            background: #5a67d8;
        }
        
        button.danger {
            background: #e53e3e;
        }
        
        button.danger:hover {
            background: #c53030;
        }
        
        input, textarea {
            width: 100%;
            padding: 0.75rem;
            border: 2px solid #e2e8f0;
            border-radius: 5px;
            font-size: 1rem;
            margin-bottom: 1rem;
            transition: border-color 0.3s ease;
        }
        
        input:focus, textarea:focus {
            outline: none;
            border-color: #667eea;
        }
        
        textarea {
            min-height: 120px;
            resize: vertical;
        }
        
        .posts-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
            gap: 20px;
            margin-top: 2rem;
        }
        
        .post-card {
            background: white;
            border-radius: 10px;
            padding: 1.5rem;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            position: relative;
        }
        
        .post-card h4 {
            color: #2d3748;
            margin-bottom: 0.5rem;
            font-size: 1.25rem;
        }
        
        .post-card p {
            color: #4a5568;
            margin-bottom: 1rem;
            font-size: 0.95rem;
        }
        
        .post-actions {
            display: flex;
            gap: 10px;
            margin-top: 1rem;
        }
        
        .post-actions button {
            flex: 1;
            padding: 0.5rem;
            font-size: 0.9rem;
            margin: 0;
        }
        
        .loading {
            text-align: center;
            padding: 2rem;
            color: #667eea;
            font-size: 1.2rem;
        }
        
        .error {
            background: #fed7d7;
            color: #9b2c2c;
            padding: 1rem;
            border-radius: 5px;
            margin: 1rem 0;
            border-left: 4px solid #e53e3e;
        }
        
        .success {
            background: #c6f6d5;
            color: #22543d;
            padding: 1rem;
            border-radius: 5px;
            margin: 1rem 0;
            border-left: 4px solid #38a169;
        }
        
        .modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0, 0, 0, 0.5);
            z-index: 1000;
            align-items: center;
            justify-content: center;
        }
        
        .modal.active {
            display: flex;
        }
        
        .modal-content {
            background: white;
            padding: 2rem;
            border-radius: 10px;
            max-width: 500px;
            width: 90%;
            max-height: 90vh;
            overflow-y: auto;
        }
        
        .status-indicator {
            padding: 0.5rem;
            border-radius: 5px;
            font-weight: 600;
            text-align: center;
            margin-bottom: 1rem;
        }
        
        .status-indicator.loading {
            background: #bee3f8;
            color: #2c5282;
        }
        
        .status-indicator.success {
            background: #c6f6d5;
            color: #22543d;
        }
        
        .status-indicator.error {
            background: #fed7d7;
            color: #9b2c2c;
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>📝 Blog API Client</h1>
            <p>CRUD Operations with JSONPlaceholder API</p>
        </header>
        
        <div class="controls">
            <div class="card">
                <h3>📬 Get All Posts</h3>
                <p>Fetch all posts from the API</p>
                <button onclick="getAllPosts()">Get All Posts</button>
            </div>
            
            <div class="card">
                <h3>🔍 Get Single Post</h3>
                <input type="number" id="post-id" placeholder="Enter Post ID" min="1">
                <button onclick="getPostById()">Get Post</button>
            </div>
            
            <div class="card">
                <h3>➕ Create New Post</h3>
                <input type="text" id="post-title" placeholder="Post Title">
                <textarea id="post-body" placeholder="Post Content"></textarea>
                <button onclick="createNewPost()">Create Post</button>
            </div>
            
            <div class="card">
                <h3>🗑️ Delete Post</h3>
                <input type="number" id="delete-id" placeholder="Enter Post ID to delete">
                <button class="danger" onclick="deletePost()">Delete Post</button>
            </div>
        </div>
        
        <div id="status"></div>
        
        <div class="posts-grid" id="posts-container">
            <!-- Posts will be loaded here -->
        </div>
    </div>
    
    <!-- Modal for editing posts -->
    <div class="modal" id="edit-modal">
        <div class="modal-content">
            <h3>✏️ Edit Post</h3>
            <input type="hidden" id="edit-id">
            <input type="text" id="edit-title" placeholder="Post Title">
            <textarea id="edit-body" placeholder="Post Content"></textarea>
            <button onclick="updatePost()">Update Post</button>
            <button onclick="closeModal()" style="background: #718096; margin-top: 10px;">Cancel</button>
        </div>
    </div>
    
    <script>
        const API_URL = 'https://jsonplaceholder.typicode.com/posts';
        const postsContainer = document.getElementById('posts-container');
        const statusDiv = document.getElementById('status');
        const editModal = document.getElementById('edit-modal');
        
        // Utility functions
        function showStatus(message, type = 'info') {
            statusDiv.innerHTML = `
                <div class="status-indicator ${type}">
                    ${message}
                </div>
            `;
            
            // Auto-hide success messages after 3 seconds
            if (type === 'success') {
                setTimeout(() => {
                    statusDiv.innerHTML = '';
                }, 3000);
            }
        }
        
        function showError(message) {
            showStatus(`${message}`, 'error');
        }
        
        function showLoading(message) {
            showStatus(` ${message}...`, 'loading');
        }
        
        function showSuccess(message) {
            showStatus(` ${message}`, 'success');
        }
        
        function closeModal() {
            editModal.classList.remove('active');
        }
        
        // API Functions
        async function fetchWithErrorHandling(url, options = {}) {
            try {
                showLoading('Loading data');
                
                const response = await fetch(url, {
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    ...options
                });
                
                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
                }
                
                const data = await response.json();
                showSuccess('Data loaded successfully');
                return data;
                
            } catch (error) {
                showError(`Failed: ${error.message}`);
                console.error('API Error:', error);
                throw error;
            }
        }
        
        // CRUD Operations
        async function getAllPosts() {
            try {
                const posts = await fetchWithErrorHandling(API_URL);
                renderPosts(posts);
            } catch (error) {
                // Error already shown by fetchWithErrorHandling
            }
        }
        
        async function getPostById() {
            const postId = document.getElementById('post-id').value;
            
            if (!postId || postId < 1) {
                showError('Please enter a valid Post ID (>= 1)');
                return;
            }
            
            try {
                const post = await fetchWithErrorHandling(`${API_URL}/${postId}`);
                renderPosts([post]); // Render as array
            } catch (error) {
                // Error already shown
            }
        }
        
        async function createNewPost() {
            const title = document.getElementById('post-title').value.trim();
            const body = document.getElementById('post-body').value.trim();
            
            if (!title || !body) {
                showError('Please fill in both title and body');
                return;
            }
            
            const newPost = {
                title,
                body,
                userId: 1 // Hardcoded for demo
            };
            
            try {
                const createdPost = await fetchWithErrorHandling(API_URL, {
                    method: 'POST',
                    body: JSON.stringify(newPost)
                });
                
                showSuccess(`Post created with ID: ${createdPost.id}`);
                document.getElementById('post-title').value = '';
                document.getElementById('post-body').value = '';
                
                // Refresh posts
                getAllPosts();
                
            } catch (error) {
                // Error already shown
            }
        }
        
        async function updatePost() {
            const id = document.getElementById('edit-id').value;
            const title = document.getElementById('edit-title').value.trim();
            const body = document.getElementById('edit-body').value.trim();
            
            if (!title || !body) {
                showError('Please fill in both title and body');
                return;
            }
            
            const updatedPost = {
                id: parseInt(id),
                title,
                body,
                userId: 1
            };
            
            try {
                const result = await fetchWithErrorHandling(`${API_URL}/${id}`, {
                    method: 'PUT',
                    body: JSON.stringify(updatedPost)
                });
                
                showSuccess('Post updated successfully');
                closeModal();
                getAllPosts();
                
            } catch (error) {
                // Error already shown
            }
        }
        
        async function deletePost() {
            const postId = document.getElementById('delete-id').value;
            
            if (!postId || postId < 1) {
                showError('Please enter a valid Post ID');
                return;
            }
            
            if (!confirm(`Are you sure you want to delete post #${postId}?`)) {
                return;
            }
            
            try {
                await fetchWithErrorHandling(`${API_URL}/${postId}`, {
                    method: 'DELETE'
                });
                
                showSuccess(`Post #${postId} deleted successfully`);
                document.getElementById('delete-id').value = '';
                getAllPosts();
                
            } catch (error) {
                // Error already shown
            }
        }
        
        // UI Rendering
        function renderPosts(posts) {
            if (!posts || posts.length === 0) {
                postsContainer.innerHTML = '<div class="loading">No posts found</div>';
                return;
            }
            
            postsContainer.innerHTML = posts.map(post => `
                <div class="post-card" data-id="${post.id}">
                    <h4>${post.title}</h4>
                    <p>${post.body.substring(0, 150)}${post.body.length > 150 ? '...' : ''}</p>
                    <div class="post-meta">
                        <small>Post ID: ${post.id} | User ID: ${post.userId}</small>
                    </div>
                    <div class="post-actions">
                        <button onclick="openEditModal(${post.id}, '${post.title.replace(/'/g, "\\'")}', '${post.body.replace(/'/g, "\\'")}')">
                            Edit
                        </button>
                        <button class="danger" onclick="confirmDelete(${post.id})">
                            Delete
                        </button>
                    </div>
                </div>
            `).join('');
        }
        
        function openEditModal(id, title, body) {
            document.getElementById('edit-id').value = id;
            document.getElementById('edit-title').value = title;
            document.getElementById('edit-body').value = body;
            editModal.classList.add('active');
        }
        
        async function confirmDelete(id) {
            if (!confirm(`Delete post #${id}?`)) {
                return;
            }
            
            try {
                await fetchWithErrorHandling(`${API_URL}/${id}`, {
                    method: 'DELETE'
                });
                
                showSuccess(`Post #${id} deleted`);
                getAllPosts();
            } catch (error) {
                // Error already shown
            }
        }
        
        // Initialize
        document.addEventListener('DOMContentLoaded', () => {
            getAllPosts();
            
            // Close modal when clicking outside
            editModal.addEventListener('click', (e) => {
                if (e.target === editModal) {
                    closeModal();
                }
            });
            
            // Close modal with Escape key
            document.addEventListener('keydown', (e) => {
                if (e.key === 'Escape' && editModal.classList.contains('active')) {
                    closeModal();
                }
            });
        });
    </script>
</body>
</html>

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?