|
|
|
|
|
|
|
|
class FlowFlexApp { |
|
|
constructor() { |
|
|
this.currentFeed = 'trending'; |
|
|
this.posts = []; |
|
|
this.seenPosts = new Set(); |
|
|
this.userPreferences = this.getUserPreferences(); |
|
|
this.isLoading = false; |
|
|
this.page = 1; |
|
|
|
|
|
this.init(); |
|
|
} |
|
|
|
|
|
init() { |
|
|
this.bindEvents(); |
|
|
this.loadInitialFeed(); |
|
|
this.setupIntersectionObserver(); |
|
|
this.checkAuthStatus(); |
|
|
} |
|
|
bindEvents() { |
|
|
|
|
|
document.querySelectorAll('.feed-filter-btn').forEach(btn => { |
|
|
btn.addEventListener('click', (e) => { |
|
|
this.switchFeed(e.target.closest('.feed-filter-btn').dataset.filter); |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('click', (e) => { |
|
|
if (e.target.closest('.like-btn')) { |
|
|
this.handleLike(e.target.closest('.like-btn')); |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('click', (e) => { |
|
|
if (e.target.closest('.save-btn')) { |
|
|
this.handleSave(e.target.closest('.save-btn')); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('click', (e) => { |
|
|
const postCard = e.target.closest('.post-card'); |
|
|
if (postCard) { |
|
|
this.trackPostInteraction(postCard.dataset.postId); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('click', (e) => { |
|
|
if (e.target.closest('[data-auth-trigger]')) { |
|
|
this.showAuthModal(); |
|
|
} |
|
|
}); |
|
|
} |
|
|
switchFeed(feedType) { |
|
|
|
|
|
document.querySelectorAll('.feed-filter-btn').forEach(btn => { |
|
|
btn.classList.remove('active', 'bg-primary-500', 'text-white'); |
|
|
btn.classList.add('bg-gray-200', 'text-gray-700'); |
|
|
}); |
|
|
|
|
|
const activeBtn = document.querySelector(`[data-filter="${feedType}"]`); |
|
|
activeBtn.classList.remove('bg-gray-200', 'text-gray-700'); |
|
|
activeBtn.classList.add('active', 'bg-primary-500', 'text-white'); |
|
|
|
|
|
this.currentFeed = feedType; |
|
|
this.page = 1; |
|
|
this.posts = []; |
|
|
this.loadFeed(); |
|
|
} |
|
|
|
|
|
async loadInitialFeed() { |
|
|
await this.loadFeed(); |
|
|
} |
|
|
|
|
|
async loadFeed() { |
|
|
if (this.isLoading) return; |
|
|
|
|
|
this.isLoading = true; |
|
|
this.showLoading(); |
|
|
|
|
|
try { |
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000)); |
|
|
|
|
|
const newPosts = await this.fetchPosts(this.currentFeed, this.page); |
|
|
const filteredPosts = this.filterDuplicatePosts(newPosts); |
|
|
|
|
|
if (filteredPosts.length > 0) { |
|
|
this.posts = [...this.posts, ...filteredPosts]; |
|
|
this.renderPosts(filteredPosts); |
|
|
this.page++; |
|
|
} else { |
|
|
this.showEmptyState(); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error loading feed:', error); |
|
|
this.showError('Failed to load posts. Please try again.'); |
|
|
} finally { |
|
|
this.isLoading = false; |
|
|
this.hideLoading(); |
|
|
} |
|
|
} |
|
|
|
|
|
async fetchPosts(feedType, page = 1) { |
|
|
|
|
|
const accessKey = 'YOUR_UNSPLASH_ACCESS_KEY'; |
|
|
const endpoints = { |
|
|
trending: `https://api.unsplash.com/photos?page=${page}&per_page=9&order_by=popular`, |
|
|
following: `https://api.unsplash.com/photos?page=${page}&per_page=9&order_by=latest`, |
|
|
discover: `https://api.unsplash.com/photos/random?count=9`, |
|
|
personalized: `https://api.unsplash.com/photos?page=${page}&per_page=9` |
|
|
}; |
|
|
|
|
|
|
|
|
return this.generateMockPosts(9); |
|
|
} |
|
|
|
|
|
generateMockPosts(count) { |
|
|
const categories = ['nature', 'technology', 'travel', 'food', 'architecture', 'people']; |
|
|
const users = [ |
|
|
{ name: 'Alex Johnson', username: 'alexj', followers: 1243 }, |
|
|
{ name: 'Sarah Miller', username: 'sarahm', followers: 856 }, |
|
|
{ name: 'Mike Chen', username: 'mikec', followers: 2107 }, |
|
|
{ name: 'Emma Davis', username: 'emmad', followers: 932 }, |
|
|
{ name: 'James Wilson', username: 'jamesw', followers: 1541 } |
|
|
]; |
|
|
|
|
|
return Array.from({ length: count }, (_, i) => { |
|
|
const user = users[Math.floor(Math.random() * users.length)]; |
|
|
const category = categories[Math.floor(Math.random() * categories.length)]; |
|
|
const postId = `post_${Date.now()}_${i}`; |
|
|
|
|
|
return { |
|
|
id: postId, |
|
|
user: user, |
|
|
image: `http://static.photos/${category}/640x360/${i + 1}`, |
|
|
caption: this.generateMockCaption(category), |
|
|
likes: Math.floor(Math.random() * 1000), |
|
|
comments: Math.floor(Math.random() * 50), |
|
|
shares: Math.floor(Math.random() * 20), |
|
|
timestamp: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000), |
|
|
category: category, |
|
|
engagement: Math.random() * 100 |
|
|
}; |
|
|
}); |
|
|
} |
|
|
|
|
|
generateMockCaption(category) { |
|
|
const captions = { |
|
|
nature: ['Beautiful sunset at the beach! 🌅', 'Morning hike in the mountains 🏔️', 'Peaceful forest walk 🌲'], |
|
|
technology: ['Working on some exciting new projects! 💻', 'Tech conference was amazing! 🚀', 'Latest gadget unboxing 📱'], |
|
|
travel: ['Exploring new places! ✈️', 'Cultural experience of a lifetime 🌍', 'Travel dreams coming true 🗺️'], |
|
|
food: ['Delicious homemade meal! 🍽️', 'Food photography session 📸', 'Trying out new recipes 👨🍳'], |
|
|
architecture: ['Modern architecture never fails to impress! 🏛️', 'Historical building tour 🏰', 'Architectural marvels 🏗️'], |
|
|
people: ['Great time with friends! 👥', 'Community event was fantastic! 🎉', 'Networking and making connections 🤝'] |
|
|
}; |
|
|
|
|
|
const categoryCaptions = captions[category] || ['Great day! 😊']; |
|
|
return categoryCaptions[Math.floor(Math.random() * categoryCaptions.length)]; |
|
|
} |
|
|
|
|
|
filterDuplicatePosts(newPosts) { |
|
|
return newPosts.filter(post => !this.seenPosts.has(post.id)); |
|
|
} |
|
|
|
|
|
renderPosts(posts) { |
|
|
const feedContainer = document.getElementById('feed-container'); |
|
|
const emptyState = document.getElementById('empty-state'); |
|
|
|
|
|
if (posts.length === 0 && this.posts.length === 0) { |
|
|
this.showEmptyState(); |
|
|
return; |
|
|
} |
|
|
|
|
|
emptyState.classList.add('hidden'); |
|
|
|
|
|
posts.forEach(post => { |
|
|
this.seenPosts.add(post.id); |
|
|
const postElement = this.createPostElement(post); |
|
|
feedContainer.appendChild(postElement); |
|
|
}); |
|
|
} |
|
|
|
|
|
createPostElement(post) { |
|
|
const postDiv = document.createElement('div'); |
|
|
postDiv.className = 'post-card bg-white rounded-xl shadow-sm overflow-hidden fade-in'; |
|
|
postDiv.dataset.postId = post.id; |
|
|
|
|
|
const timeAgo = this.getTimeAgo(post.timestamp); |
|
|
|
|
|
postDiv.innerHTML = ` |
|
|
<div class="relative"> |
|
|
<img src="${post.image}" alt="Post image" class="w-full h-48 object-cover"> |
|
|
<button class="save-btn absolute top-3 right-3 p-2 bg-white/80 backdrop-blur-sm rounded-full hover:bg-white transition-colors duration-200"> |
|
|
<i data-feather="bookmark" class="w-4 h-4 text-gray-600"></i> |
|
|
</button> |
|
|
</div> |
|
|
<div class="p-4"> |
|
|
<div class="flex items-center justify-between mb-3"> |
|
|
<div class="flex items-center space-x-3"> |
|
|
<img src="http://static.photos/people/40x40/${Math.floor(Math.random() * 100)}" alt="${post.user.name}" class="w-8 h-8 rounded-full"> |
|
|
<div> |
|
|
<h4 class="font-semibold text-gray-900 text-sm">${post.user.name}</h4> |
|
|
<p class="text-gray-500 text-xs">@${post.user.username}</p> |
|
|
</div> |
|
|
</div> |
|
|
<span class="text-xs text-gray-400">${timeAgo}</span> |
|
|
</div> |
|
|
|
|
|
<p class="text-gray-700 mb-4 text-sm">${post.caption}</p> |
|
|
|
|
|
<div class="flex items-center justify-between text-gray-500"> |
|
|
<div class="flex items-center space-x-4"> |
|
|
<button class="like-btn flex items-center space-x-1 text-sm hover:text-red-500 transition-colors duration-200"> |
|
|
<i data-feather="heart" class="w-4 h-4"></i> |
|
|
<span>${post.likes}</span> |
|
|
</button> |
|
|
<button class="flex items-center space-x-1 text-sm hover:text-blue-500 transition-colors duration-200"> |
|
|
<i data-feather="message-circle" class="w-4 h-4"></i> |
|
|
<span>${post.comments}</span> |
|
|
</button> |
|
|
<button class="flex items-center space-x-1 text-sm hover:text-green-500 transition-colors duration-200"> |
|
|
<i data-feather="share-2" class="w-4 h-4"></i> |
|
|
<span>${post.shares}</span> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
return postDiv; |
|
|
} |
|
|
|
|
|
handleLike(likeBtn) { |
|
|
const heartIcon = likeBtn.querySelector('i'); |
|
|
const likesCount = likeBtn.querySelector('span'); |
|
|
|
|
|
likeBtn.classList.add('like-animation'); |
|
|
setTimeout(() => likeBtn.classList.remove('like-animation'), 600); |
|
|
|
|
|
const isLiked = heartIcon.style.fill === 'currentColor'; |
|
|
const currentLikes = parseInt(likesCount.textContent); |
|
|
|
|
|
if (isLiked) { |
|
|
heartIcon.style.fill = 'none'; |
|
|
likesCount.textContent = currentLikes - 1; |
|
|
likesCount.style.color = ''; |
|
|
} else { |
|
|
heartIcon.style.fill = 'currentColor'; |
|
|
likesCount.textContent = currentLikes + 1; |
|
|
likesCount.style.color = '#ef4444'; |
|
|
} |
|
|
} |
|
|
|
|
|
handleSave(saveBtn) { |
|
|
const bookmarkIcon = saveBtn.querySelector('i'); |
|
|
const isSaved = bookmarkIcon.style.fill === 'currentColor'; |
|
|
|
|
|
if (isSaved) { |
|
|
bookmarkIcon.style.fill = 'none'; |
|
|
} else { |
|
|
bookmarkIcon.style.fill = 'currentColor'; |
|
|
} |
|
|
} |
|
|
|
|
|
trackPostInteraction(postId) { |
|
|
|
|
|
const post = this.posts.find(p => p.id === postId); |
|
|
if (post) { |
|
|
this.updateUserPreferences(post); |
|
|
} |
|
|
} |
|
|
|
|
|
updateUserPreferences(post) { |
|
|
if (!this.userPreferences.engagedCategories) { |
|
|
this.userPreferences.engagedCategories = {}; |
|
|
} |
|
|
|
|
|
this.userPreferences.engagedCategories[post.category] = |
|
|
(this.userPreferences.engagedCategories[post.category] || 0) + 1; |
|
|
|
|
|
localStorage.setItem('flowflex_preferences', JSON.stringify(this.userPreferences)); |
|
|
} |
|
|
|
|
|
getUserPreferences() { |
|
|
const stored = localStorage.getItem('flowflex_preferences'); |
|
|
return stored ? JSON.parse(stored) : { |
|
|
engagedCategories: {}, |
|
|
preferredFeed: 'trending', |
|
|
blockedUsers: [] |
|
|
}; |
|
|
} |
|
|
|
|
|
getTimeAgo(timestamp) { |
|
|
const now = new Date(); |
|
|
const diff = now - new Date(timestamp); |
|
|
const minutes = Math.floor(diff / 60000); |
|
|
const hours = Math.floor(diff / 3600000); |
|
|
const days = Math.floor(diff / 86400000); |
|
|
|
|
|
if (days > 0) return `${days}d ago`; |
|
|
if (hours > 0) return `${hours}h ago`; |
|
|
return `${minutes}m ago`; |
|
|
} |
|
|
|
|
|
showLoading() { |
|
|
document.getElementById('loading').classList.remove('hidden'); |
|
|
} |
|
|
|
|
|
hideLoading() { |
|
|
document.getElementById('loading').classList.add('hidden'); |
|
|
} |
|
|
|
|
|
showEmptyState() { |
|
|
document.getElementById('feed-container').innerHTML = ''; |
|
|
document.getElementById('empty-state').classList.remove('hidden'); |
|
|
} |
|
|
showError(message) { |
|
|
|
|
|
const errorDiv = document.createElement('div'); |
|
|
errorDiv.className = 'fixed top-4 right-4 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg z-50'; |
|
|
errorDiv.textContent = message; |
|
|
document.body.appendChild(errorDiv); |
|
|
|
|
|
setTimeout(() => { |
|
|
errorDiv.remove(); |
|
|
}, 3000); |
|
|
} |
|
|
|
|
|
setupIntersectionObserver() { |
|
|
const observer = new IntersectionObserver((entries) => { |
|
|
entries.forEach(entry => { |
|
|
if (entry.isIntersecting && !this.isLoading) { |
|
|
this.loadFeed(); |
|
|
} |
|
|
}, { |
|
|
rootMargin: '100px' |
|
|
}); |
|
|
|
|
|
observer.observe(document.getElementById('loading')); |
|
|
} |
|
|
|
|
|
checkAuthStatus() { |
|
|
const isLoggedIn = localStorage.getItem('flowflex_user'); |
|
|
if (!isLoggedIn) { |
|
|
|
|
|
console.log('User not logged in'); |
|
|
} |
|
|
} |
|
|
|
|
|
showAuthModal() { |
|
|
const authModal = document.querySelector('auth-modal'); |
|
|
if (authModal) { |
|
|
authModal.show(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
loadProfilePosts() { |
|
|
|
|
|
const posts = this.generateMockPosts(12); |
|
|
this.renderProfilePosts(posts); |
|
|
} |
|
|
|
|
|
renderProfilePosts(posts) { |
|
|
const profileContent = document.getElementById('profile-content'); |
|
|
if (!profileContent) return; |
|
|
|
|
|
posts.forEach(post => { |
|
|
const postElement = this.createPostElement(post); |
|
|
profileContent.appendChild(postElement); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
new FlowFlexApp(); |
|
|
}); |
|
|
|
|
|
|
|
|
async function apiCall(url, options = {}) { |
|
|
try { |
|
|
const response = await fetch(url, { |
|
|
headers: { |
|
|
'Authorization': 'Client-ID YOUR_UNSPLASH_ACCESS_KEY', |
|
|
'Accept-Version': 'v1' |
|
|
}, |
|
|
...options |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`API call failed: ${response.status}`); |
|
|
} |
|
|
|
|
|
return await response.json(); |
|
|
} catch (error) { |
|
|
console.error('API call error:', error); |
|
|
throw error; |
|
|
} |
|
|
} |