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>