Stream

FX Gossip - Feed * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: Arial, sans-serif; background: #f0f0f0; min-height: 100vh; color: #333; } .header { background: #007bff; color: white; padding: 10px; position: sticky; top: 0; text-align: center; display: flex; justify-content: space-between; align-items: center; } .header a { color: white; text-decoration: none; padding: 8px; } .header a:hover { background: #0056b3; } .post-form { background: white; padding: 15px; margin: 10px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .post-form input[type="file"], .post-form textarea { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; } .post-form textarea { min-height: 60px; resize: vertical; } .post-form button { width: 100%; padding: 10px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } .post-form button:hover { background: #0056b3; } .feed { max-width: 600px; margin: 0 auto; padding: 10px; } .post { background: white; margin-bottom: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 15px; } .post-header { display: flex; align-items: center; margin-bottom: 10px; } .avatar { width: 40px; height: 40px; border-radius: 50%; margin-right: 10px; object-fit: cover; } .post-meta h3 { margin: 0; font-size: 16px; } .post-meta .username { font-size: 14px; color: #666; } .post img { max-width: 100%; border-radius: 4px; margin: 10px 0; } .caption { margin-bottom: 10px; } .timestamp { color: #666; font-size: 12px; } .post-actions { display: flex; gap: 10px; flex-wrap: wrap; } .action-btn { padding: 8px; background: #f0f0f0; border: none; border-radius: 4px; cursor: pointer; display: flex; align-items: center; gap: 5px; } .action-btn:hover { background: #e0e0e0; } .comments { margin-top: 10px; padding-left: 20px; } .comment { font-size: 14px; margin-bottom: 5px; } .comment-input { width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #ccc; border-radius: 4px; } .error { color: red; font-size: 12px; margin-bottom: 10px; } FX Gossip Feed Profile Logout Post // Utility to escape HTML to prevent XSS function escapeHTML(str) { return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); }
    // Safe JSON parsing with error handling
    function safeParseJSON(key, defaultValue) {
        try {
            const data = localStorage.getItem(key);
            return data ? JSON.parse(data) : defaultValue;
        } catch (e) {
            console.error(`Error parsing ${key}:`, e);
            return defaultValue;
        }
    }

    // Safe localStorage save with error handling
    function safeSaveJSON(key, data) {
        try {
            localStorage.setItem(key, JSON.stringify(data));
        } catch (e) {
            console.error(`Error saving ${key}:`, e);
            alert('Failed to save data. Please try again.');
        }
    }

    // Check session and profile
    const session = safeParseJSON('session', null);
    if (!session || !session.user) {
        window.location.href = 'index.html';
        return;
    }
    const user = session.user;
    const profileKey = `profile_${user.username}`;
    const profile = safeParseJSON(profileKey, {});
    if (!profile.displayName) {
        window.location.href = 'profile.html';
        return;
    }

    // Initialize data
    let posts = safeParseJSON('posts', []);
    let follows = safeParseJSON(`follows_${user.username}`, []);
    let likes = safeParseJSON(`likes_${user.username}`, []);
    let comments = safeParseJSON('comments', []);

    function saveData() {
        safeSaveJSON('posts', posts);
        safeSaveJSON(`follows_${user.username}`, follows);
        safeSaveJSON(`likes_${user.username}`, likes);
        safeSaveJSON('comments', comments);
    }

    function renderPosts() {
        const feed = document.getElementById('feed');
        feed.innerHTML = '';
        const sortedPosts = posts.slice().sort((a, b) => {
            const aFollowed = follows.includes(a.username);
            const bFollowed = follows.includes(b.username);
            if (aFollowed && !bFollowed) return -1;
            if (!aFollowed && bFollowed) return 1;
            return b.timestamp - a.timestamp || b.id.localeCompare(a.id);
        });
        sortedPosts.forEach(post => {
            const postLikes = likes.filter(l => l.postId === post.id).length;
            const postComments = comments.filter(c => c.postId === post.id);
            const isFollowing = follows.includes(post.username);
            const isOwnPost = post.username === user.username;
            const postDiv = document.createElement('div');
            postDiv.className = 'post';
            const profile = safeParseJSON(`profile_${post.username}`, {});
            postDiv.innerHTML = `
                <div class="post-header">
                    <img class="avatar" src="${profile.profilePic || 'data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"%23333\" d=\"M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z\"/></svg>'}" alt="Avatar">
                    <div class="post-meta">
                        <h3>${escapeHTML(profile.displayName || post.username)}</h3>
                        <p class="username">@${escapeHTML(post.username)}</p>
                    </div>
                </div>
                ${post.image ? `<img src="${post.image}" alt="Post image">` : ''}
                <p class="caption">${escapeHTML(post.caption || '')}</p>
                <p class="timestamp">${new Date(post.timestamp).toLocaleString()}</p>
                <div class="post-actions">
                    <button class="action-btn like" data-post-id="${post.id}">❤️ Like (${postLikes})</button>
                    <button class="action-btn comment" data-post-id="${post.id}">💬 Comment</button>
                    <button class="action-btn share" data-post-id="${post.id}">📤 Share</button>
                    ${!isOwnPost ? `<button class="action-btn follow" data-username="${post.username}">${isFollowing ? 'Unfollow' : 'Follow'}</button>` : ''}
                </div>
                <div class="comments">
                    ${postComments.map(c => `<div class="comment">${escapeHTML(c.username)}: ${escapeHTML(c.comment)}</div>`).join('')}
                    <input type="text" class="comment-input" placeholder="Add a comment..." data-post-id="${post.id}">
                </div>
            `;
            feed.appendChild(postDiv);
        });
    }

    // Event delegation for feed actions
    document.getElementById('feed').addEventListener('click', (e) => {
        const btn = e.target.closest('.action-btn');
        if (!btn) return;
        const postId = btn.dataset.postId;
        const username = btn.dataset.username;

        if (btn.classList.contains('like')) {
            if (!likes.some(l => l.postId === postId && l.userId === user.username)) {
                likes.push({ postId, userId: user.username });
                saveData();
                renderPosts();
            }
        } else if (btn.classList.contains('comment')) {
            const input = btn.parentElement.nextElementSibling.querySelector('.comment-input');
            input.focus();
        } else if (btn.classList.contains('share')) {
            const postUrl = `${window.location.origin}/feed.html#post-${postId}`;
            if (navigator.clipboard) {
                navigator.clipboard.writeText(postUrl).then(() => alert('Link copied!')).catch(() => alert('Failed to copy link.'));
            } else {
                prompt('Copy this link:', postUrl);
            }
        } else if (btn.classList.contains('follow')) {
            if (follows.includes(username)) {
                follows = follows.filter(u => u !== username);
            } else {
                follows.push(username);
            }
            saveData();
            renderPosts();
        }
    });

    // Event delegation for comment input
    document.getElementById('feed').addEventListener('keypress', (e) => {
        if (e.target.classList.contains('comment-input') && e.key === 'Enter' && e.target.value.trim()) {
            comments.push({
                postId: e.target.dataset.postId,
                username: user.username,
                comment: e.target.value.trim(),
                timestamp: Date.now()
            });
            e.target.value = '';
            saveData();
            renderPosts();
        }
    });

    document.getElementById('postForm').addEventListener('submit', function(e) {
        e.preventDefault();
        const imageInput = document.getElementById('imageInput');
        const captionInput = document.getElementById('captionInput');
        const errorDiv = document.getElementById('error');
        errorDiv.textContent = '';

        if (imageInput.files.length > 0) {
            const file = imageInput.files[0];
            const validTypes = ['image/jpeg', 'image/png', 'image/gif'];
            const maxSize = 5 * 1024 * 1024; // 5MB
            if (!validTypes.includes(file.type)) {
                errorDiv.textContent = 'Please upload a valid image (JPEG, PNG, or GIF).';
                return;
            }
            if (file.size > maxSize) {
                errorDiv.textContent = 'Image size must be less than 5MB.';
                return;
            }
            const reader = new FileReader();
            reader.onload = function(e) {
                posts.push({
                    id: Date.now().toString(),
                    username: user.username,
                    image: e.target.result,
                    caption: captionInput.value.trim(),
                    timestamp: Date.now()
                });
                captionInput.value = '';
                imageInput.value = '';
                saveData();
                renderPosts();
            };
            reader.onerror = function() {
                errorDiv.textContent = 'Error reading the image file. Please try again.';
            };
            reader.readAsDataURL(file);
        }
    });

    document.getElementById('logout').addEventListener('click', function(e) {
        e.preventDefault();
        localStorage.removeItem('session');
        window.location.href = 'index.html';
    });

    renderPosts();
</script>