Just Finished

Loading posts...
const email = document.getElementById('loginEmail').value const password = document.getElementById('loginPassword').value hideError('loginError') try { const { data, error } = await supabaseClient.auth.signInWithPassword({ email: email, password: password, }) if (error) throw error closeModal('loginModal') updateAuthUI() loadPosts() } catch (error) { showError('loginError', handleError(error, 'Login')) } } async function logout() { if (!supabaseClient) return try { const { error } = await supabaseClient.auth.signOut() if (!error) { currentUser = null updateAuthUI() loadPosts() } } catch (error) { console.error('Logout error:', error) } } function updateAuthUI() { const authButtons = document.querySelector('.auth-buttons') const userInfo = document.querySelector('.user-info') if (!authButtons || !userInfo) return if (currentUser) { authButtons.style.display = 'none' userInfo.style.display = 'flex' const avatar = document.getElementById('headerAvatar') const username = document.getElementById('headerUsername') if (avatar && username) { avatar.src = currentUser.avatar_url || 'https://ui-avatars.com/api/?name=' + encodeURIComponent(currentUser.username) + '&background=667eea&color=fff&size=40' username.textContent = '@' + currentUser.username } } else { authButtons.style.display = 'flex' userInfo.style.display = 'none' } } // Post management functions async function handleCreatePost(event) { event.preventDefault() if (!currentUser) { openModal('loginModal') return } if (!supabaseClient) { showError('createPostError', 'Service not available. Please refresh the page.') return } const title = document.getElementById('postTitle').value const category = document.getElementById('postCategory').value const content = document.getElementById('postContent').value hideError('createPostError') try { const processedContent = processSpoilerTags(content) const { data, error } = await supabaseClient .from('posts') .insert([ { title: title, content: processedContent, category: category, author_id: currentUser.id, author_username: currentUser.username, votes: 0, created_at: new Date().toISOString() } ]) .select() if (error) throw error closeModal('createPostModal') document.getElementById('postTitle').value = '' document.getElementById('postContent').value = '' document.getElementById('postCategory').value = '' loadPosts() updateCategoryCounts() } catch (error) { showError('createPostError', handleError(error, 'Create post')) } } function processSpoilerTags(content) { if (!content) return '' return content.replace(/\[spoiler\](.*?)\[\/spoiler\]/g, '$1') } async function loadPosts() { const postsLoading = document.getElementById('postsLoading') const postsList = document.getElementById('postsList') if (!postsList) return if (postsLoading) postsLoading.style.display = 'block' postsList.innerHTML = '' if (!supabaseClient || SUPABASE_URL === 'YOUR_SUPABASE_URL_HERE') { displayDemoPosts() if (postsLoading) postsLoading.style.display = 'none' return } try { let query = supabaseClient .from('posts') .select('*, profiles!posts_author_id_fkey(username, avatar_url)') .order('created_at', { ascending: false }) if (currentCategory !== 'all') { query = query.eq('category', currentCategory) } const { data: posts, error } = await query if (error) throw error allPosts = posts || [] displayPosts(allPosts) } catch (error) { console.error('Load posts error:', error) postsList.innerHTML = '
Error loading posts. Please refresh the page.
' } finally { if (postsLoading) postsLoading.style.display = 'none' } } function displayDemoPosts() { const demoPosts = [ { id: 'demo1', title: 'The ending of "The Bear" Season 3', content: 'Carmen finally realizes that success means finding balance between passion and mental health - What did everyone think of this character development?', category: 'tv', author_username: 'ChefLife2024', votes: 42, created_at: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), profiles: { avatar_url: null } }, { id: 'demo2', title: 'Anyone else confused by Inception\'s ending?', content: 'The spinning top was actually Mal\'s totem, not Dom\'s. His real totem was his wedding ring - This changes everything about the final scene!', category: 'movies', author_username: 'MovieBuff87', votes: 28, created_at: new Date(Date.now() - 5 * 60 * 60 * 1000).toISOString(), profiles: { avatar_url: null } }, { id: 'demo3', title: 'House of Dragon finale predictions were WRONG', content: 'Daemon actually survives and becomes the new king after everyone else dies - I did not see that coming at all!', category: 'tv', author_username: 'DragonQueen', votes: 156, created_at: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), profiles: { avatar_url: null } } ] displayPosts(demoPosts) } function displayPosts(posts) { const postsList = document.getElementById('postsList') if (!postsList) return if (posts.length === 0) { postsList.innerHTML = '

No posts yet!

Be the first to share some spoilers!

' return } postsList.innerHTML = posts.map(post => { const avatarUrl = (post.profiles && post.profiles.avatar_url) || 'https://ui-avatars.com/api/?name=' + encodeURIComponent(post.author_username) + '&background=667eea&color=fff&size=32' return '
' + '
' + '
' + '' + '
' + (post.votes || 0) + '
' + '' + '
' + '
' + '

' + escapeHtml(post.title) + '

' + '' + '
' + (post.content || '') + '
' + '
' + '
' + '
' }).join('') } async function vote(postId, direction) { if (!currentUser) { openModal('loginModal') return } if (!supabaseClient || SUPABASE_URL === 'YOUR_SUPABASE_URL_HERE') { const voteCountElement = document.getElementById('votes-' + postId) if (voteCountElement) { const currentVotes = parseInt(voteCountElement.textContent) || 0 const newVotes = direction === 'up' ? currentVotes + 1 : Math.max(0, currentVotes - 1) voteCountElement.textContent = newVotes } return } try { const { data: existingVote } = await supabaseClient .from('votes') .select('*') .eq('post_id', postId) .eq('user_id', currentUser.id) .single() let voteChange = 0 if (existingVote) { if (existingVote.vote_type === direction) { await supabaseClient .from('votes') .delete() .eq('post_id', postId) .eq('user_id', currentUser.id) voteChange = direction === 'up' ? -1 : 1 } else { await supabaseClient .from('votes') .update({ vote_type: direction }) .eq('post_id', postId) .eq('user_id', currentUser.id) voteChange = direction === 'up' ? 2 : -2 } } else { await supabaseClient .from('votes') .insert([{ post_id: postId, user_id: currentUser.id, vote_type: direction }]) voteChange = direction === 'up' ? 1 : -1 } const { data: currentPost } = await supabaseClient .from('posts') .select('votes') .eq('id', postId) .single() const newVoteCount = (currentPost && currentPost.votes || 0) + voteChange await supabaseClient .from('posts') .update({ votes: newVoteCount }) .eq('id', postId) const voteCountElement = document.getElementById('votes-' + postId) if (voteCountElement) { voteCountElement.textContent = newVoteCount } } catch (error) { console.error('Vote error:', error) } } // Utility functions function escapeHtml(text) { if (!text) return '' const div = document.createElement('div') div.textContent = text return div.innerHTML } function formatDate(dateString) { if (!dateString) return 'Unknown' try { const date = new Date(dateString) const now = new Date() const diffMs = now - date const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) const diffDays = Math.floor(diffHours / 24) if (diffHours < 1) return 'Just now' if (diffHours < 24) return diffHours + 'h ago' if (diffDays < 7) return diffDays + 'd ago' return date.toLocaleDateString() } catch (error) { return 'Unknown' } } function revealSpoiler(element) { if (!element || element.classList.contains('revealed')) return element.style.transform = 'scale(0.95)' setTimeout(function() { element.classList.add('revealed') element.style.transform = 'scale(1.05)' setTimeout(function() { element.style.transform = 'scale(1)' }, 200) }, 100) } // Search and filtering functions async function performSearch(query) { const searchResults = document.getElementById('searchResults') const searchResultsList = document.getElementById('searchResultsList') if (!searchResults || !searchResultsList) return if (!query || !query.trim()) { searchResults.style.display = 'none' return } if (!supabaseClient || SUPABASE_URL === 'YOUR_SUPABASE_URL_HERE') { const demoResults = allPosts.filter(function(post) { return post.title.toLowerCase().indexOf(query.toLowerCase()) !== -1 || post.author_username.toLowerCase().indexOf(query.toLowerCase()) !== -1 }) if (demoResults.length === 0) { searchResultsList.innerHTML = '
No results found
' } else { searchResultsList.innerHTML = demoResults.slice(0, 5).map(function(post) { return '
' + '' + escapeHtml(post.title) + '
' + 'by @' + post.author_username + ' in ' + post.category + '' + '
' }).join('') } searchResults.style.display = 'block' return } try { const { data: posts, error } = await supabaseClient .from('posts') .select('*, profiles!posts_author_id_fkey(username)') .or('title.ilike.%' + query + '%, content.ilike.%' + query + '%, author_username.ilike.%' + query + '%') .order('created_at', { ascending: false }) .limit(10) if (error) throw error if (posts.length === 0) { searchResultsList.innerHTML = '
No results found
' } else { searchResultsList.innerHTML = posts.map(function(post) { return '
' + '' + escapeHtml(post.title) + '
' + 'by @' + post.author_username + ' in ' + post.category + '' + '
' }).join('') } searchResults.style.display = 'block' } catch (error) { searchResultsList.innerHTML = '
Search error
' } } function filterByCategory(category) { currentCategory = category loadPosts() } function showAllPosts() { currentCategory = 'all' loadPosts() } async function updateCategoryCounts() { const categories = ['movies', 'tv', 'books', 'games', 'anime'] if (!supabaseClient || SUPABASE_URL === 'YOUR_SUPABASE_URL_HERE') { const demoCounts = { movies: 342, tv: 587, books: 234, games: 189, anime: 456 } categories.forEach(function(category) { const countElement = document.getElementById(category + 'Count') if (countElement) { countElement.textContent = demoCounts[category] + ' posts' } }) return } for (let i = 0; i < categories.length; i++) { const category = categories[i] try { const { count } = await supabaseClient .from('posts') .select('*', { count: 'exact', head: true }) .eq('category', category) const countElement = document.getElementById(category + 'Count') if (countElement) { countElement.textContent = (count || 0) + ' posts' } } catch (error) { console.error('Error counting ' + category + ':', error) } } } // User profile functions async function showProfile(username) { const profileContent = document.getElementById('profileContent') if (!profileContent) return profileContent.innerHTML = '
Loading profile...
' openModal('profileModal') if (!supabaseClient || SUPABASE_URL === 'YOUR_SUPABASE_URL_HERE') { const demoProfiles = { 'ChefLife2024': { username: 'ChefLife2024', bio: 'Professional chef who loves discussing cooking shows and food-related media.', created_at: '2024-03-01', avatar_url: null }, 'MovieBuff87': { username: 'MovieBuff87', bio: 'Cinema enthusiast since 1987. I live for plot twists and mind-bending endings.', created_at: '2024-01-15', avatar_url: null }, 'DragonQueen': { username: 'DragonQueen', bio: 'Fantasy fanatic and theory crafter. If there is a dragon in it, I have probably watched it.', created_at: '2024-02-10', avatar_url: null } } const profile = demoProfiles[username] || { username: username, bio: 'Active community member', created_at: '2024-01-01', avatar_url: null } profileContent.innerHTML = '
' + '' + '
' + '

@' + profile.username + '

' + '
Demo User • Active Member
' + '
Joined ' + formatDate(profile.created_at) + '
' + '
' + '
' + (profile.bio ? '
About:
' + escapeHtml(profile.bio) + '
' : '') + '
Spoiler Expert • Active community member
' return } try { const { data: profile, error } = await supabaseClient .from('profiles') .select('*') .eq('username', username) .single() if (error) throw error const { count: postCount } = await supabaseClient .from('posts') .select('*', { count: 'exact', head: true }) .eq('author_username', username) const { data: totalVotes } = await supabaseClient .from('posts') .select('votes') .eq('author_username', username) const totalUpvotes = (totalVotes || []).reduce(function(sum, post) { return sum + (post.votes || 0) }, 0) profileContent.innerHTML = '
' + '' + '
' + '

@' + profile.username + '

' + '
' + (postCount || 0) + ' posts • ' + totalUpvotes + ' upvotes received
' + '
Joined ' + formatDate(profile.created_at) + '
' + '
' + '
' + (profile.bio ? '
About:
' + escapeHtml(profile.bio) + '
' : '') + '
Spoiler Expert • Active community member
' } catch (error) { profileContent.innerHTML = '
Error loading profile: ' + handleError(error, 'Load profile') + '
' } } async function loadOnlineUsers() { const onlineUsersList = document.getElementById('onlineUsersList') if (!onlineUsersList) return if (!supabaseClient || SUPABASE_URL === 'YOUR_SUPABASE_URL_HERE') { const demoUsers = [ { username: 'SpoilerMaster', avatar_url: null }, { username: 'PlotTwistFan', avatar_url: null }, { username: 'EndingExpert', avatar_url: null }, { username: 'BookFinisher', avatar_url: null }, { username: 'TVAddict99', avatar_url: null } ] onlineUsersList.innerHTML = demoUsers.map(function(user) { return '
' + '' + '' + user.username + '' + '@' + user.username + '' + '
' }).join('') return } try { const { data: users, error } = await supabaseClient .from('profiles') .select('username, avatar_url') .order('created_at', { ascending: false }) .limit(10) if (error) throw error if (users.length === 0) { onlineUsersList.innerHTML = '
No users found
' } else { onlineUsersList.innerHTML = users.map(function(user) { return '
' + '' + '' + user.username + '' + '@' + user.username + '' + '
' }).join('') } } catch (error) { onlineUsersList.innerHTML = '
Error loading users
' } } // Modal functions function openModal(modalId) { const modal = document.getElementById(modalId) if (modal) { modal.style.display = 'flex' } } function closeModal(modalId) { const modal = document.getElementById(modalId) if (modal) { modal.style.display = 'none' } const errorDivs = document.querySelectorAll('.error') errorDivs.forEach(function(div) { div.style.display = 'none' }) } function openCreatePost() { if (!currentUser) { openModal('loginModal') return } openModal('createPostModal') } // Initialization async function initializeApp() { console.log('Initializing Just Finished forum...') if (!supabaseClient || SUPABASE_URL === 'YOUR_SUPABASE_URL_HERE' || SUPABASE_ANON_KEY === 'YOUR_SUPABASE_ANON_KEY_HERE') { console.log('Running in demo mode - Supabase not configured') updateAuthUI() loadPosts() updateCategoryCounts() loadOnlineUsers() return } try { const { data: { session } } = await supabaseClient.auth.getSession() if (session) { const { data: profile } = await supabaseClient .from('profiles') .select('*') .eq('id', session.user.id) .single() currentUser = profile } updateAuthUI() loadPosts() updateCategoryCounts() loadOnlineUsers() supabaseClient .channel('posts') .on('postgres_changes', { event: '*', schema: 'public', table: 'posts' }, function() { loadPosts() updateCategoryCounts() }) .subscribe() supabaseClient.auth.onAuthStateChange(async function(event, session) { if (event === 'SIGNED_IN' && session) { const { data: profile } = await supabaseClient .from('profiles') .select('*') .eq('id', session.user.id) .single() currentUser = profile updateAuthUI() } else if (event === 'SIGNED_OUT') { currentUser = null updateAuthUI() } }) } catch (error) { console.error('Initialization error:', error) loadPosts() updateCategoryCounts() loadOnlineUsers() } } // Event listeners document.addEventListener('DOMContentLoaded', function() { console.log('DOM loaded, starting Just Finished forum...') initializeApp() const searchInput = document.getElementById('searchInput') if (searchInput) { let searchTimeout searchInput.addEventListener('input', function(e) { clearTimeout(searchTimeout) searchTimeout = setTimeout(function() { performSearch(e.target.value) }, 300) }) } document.addEventListener('click', function(e) { if (!e.target.closest('.search-container')) { const searchResults = document.getElementById('searchResults') if (searchResults) { searchResults.style.display = 'none' } } }) console.log('Just Finished forum loaded successfully!') setTimeout(function() { if (SUPABASE_URL === 'YOUR_SUPABASE_URL_HERE') { console.log('Demo mode: Replace Supabase credentials to enable full functionality') } else { console.log('Production mode: Supabase connected') } }, 1000) }) // Global error handlers window.addEventListener('error', function(event) { console.error('Global error:', event.error) return true }) window.addEventListener('unhandledrejection', function(event) { console.error('Unhandled promise rejection:', event.reason) event.preventDefault() }) searchResults) { searchResults.style.display = 'none' } } }) window.addEventListener('click', function(event) { if (event.target && event.target.classList && event.target.classList.contains('modal')) { event.target.style.display = 'none' } }) const categoryCards = document.querySelectorAll('.category-card') for (let i = 0; i < categoryCards.length; i++) { const card = categoryCards[i] card.addEventListener('mouseenter', function() { this.style.transform = 'translateY(-5px) scale(1.02)' }) card.addEventListener('mouseleave', function() { this.style.transform = 'translateY(0) scale(1)' }) } document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { const modals = document.querySelectorAll('.modal') for (let i = 0; i < modals.length; i++) { modals[i].style.display = 'none' } const searchResults = document.getElementById('searchResults') if ( Just Finished - Spill All The Spoilers

🚨 SPOILER ZONE 🚨

Finally finished that show? Read the last page? SPILL IT ALL! This is your safe space to discuss endings, plot twists, and everything in between.

🔍

Search Results

📝 Share Your Spoilers

Got spoilers to spill? Click here to create a new discussion!

Recent Discussions

Loading spoiler discussions...