Learn Together,
Grow Together

Join thousands of learners in our language and music communities. Share progress, ask questions, find study partners.

ut type="text" id="postLink" placeholder="Optional: paste a YouTube or image link..." style="width:100%;padding:.5rem .8rem;border-radius:.6rem;border:1.5px solid var(--bd);background:var(--cream);font-family:DM Sans,sans-serif;font-size:.82rem;outline:none;" oninput="previewLink()"/> preview
'; } } function renderTagRow() { var row = document.getElementById('tagRow'); row.innerHTML = ''; TAGS.forEach(function(t) { var b = document.createElement('button'); b.type = 'button'; b.className = 'tag-btn' + (postTags.indexOf(t) >= 0 ? ' sel' : ''); b.textContent = t; b.onclick = function() { toggleTag(t, b); }; row.appendChild(b); }); } function toggleTag(tag, btn) { var idx = postTags.indexOf(tag); if (idx >= 0) postTags.splice(idx, 1); else postTags = [tag]; renderTagRow(); } function switchComm(comm, btn) { currentComm = comm; document.querySelectorAll('.comm-btn').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); renderFeed(); } function updateCounts() { var all = window.FluenzyDB.getPosts(); document.getElementById('cnt-all').textContent = all.length; ['French','Spanish','German','Guitar','Piano','Singing'].forEach(function(c) { var el = document.getElementById('cnt-' + c); if (el) el.textContent = all.filter(function(p) { return p.community === c; }).length; }); } function timeAgo(ts) { var d = Math.floor((Date.now() - new Date(ts).getTime()) / 1000); if (d < 60) return 'just now'; if (d < 3600) return Math.floor(d/60) + 'm ago'; if (d < 86400) return Math.floor(d/3600) + 'h ago'; return Math.floor(d/86400) + 'd ago'; } function previewLink() { var url = document.getElementById('postLink').value.trim(); var img = document.getElementById('imgPreview'); if (url.match(/\.(jpg|jpeg|png|gif|webp)$/i)) { img.src = url; img.style.display = 'block'; } else img.style.display = 'none'; } function submitPost() { if (!currentUser) { alert('Please log in to post.'); return; } var text = document.getElementById('postText').value.trim(); if (!text) { alert('Please write something!'); return; } var comm = currentComm === 'all' ? 'French' : currentComm; var link = document.getElementById('postLink').value.trim(); window.FluenzyDB.savePost({ community: comm, authorName: currentUser.name, authorId: currentUser.id, authorRole: currentRole, body: text, tag: postTags[0] || '', mediaLink: link }); document.getElementById('postText').value = ''; document.getElementById('postLink').value = ''; document.getElementById('imgPreview').style.display = 'none'; postTags = []; renderTagRow(); renderFeed(); updateCounts(); } function renderFeed() { var posts = window.FluenzyDB.getPosts(currentComm === 'all' ? null : currentComm); var con = document.getElementById('feedContainer'); if (!posts.length) { con.innerHTML = '
💬

No posts yet in this community.
Be the first to share!

'; return; } con.innerHTML = posts.map(function(p) { var av = p.authorName ? p.authorName.charAt(0).toUpperCase() : '?'; var cc = COMM_COLORS[p.community] || ''; var ico = COMM_ICONS[p.community] || '🌐'; var liked = currentUser && (p.likedBy || []).indexOf(currentUser.id) >= 0; var repliesHtml = ''; (p.replies || []).forEach(function(r) { repliesHtml += '
' + (r.authorName||'?').charAt(0) + '
' + esc(r.authorName||'?') + '
' + esc(r.body) + '
'; }); var mediaHtml = ''; if (p.mediaLink && p.mediaLink.match(/\.(jpg|jpeg|png|gif|webp)$/i)) { mediaHtml = 'post image'; } return '
' + '
' + '
' + av + '
' + '
' + '
' + '' + ico + ' ' + esc(p.community) + '' + '
' + mediaHtml + '
' + esc(p.body) + '
' + '
' + '' + '' + '' + (currentUser && (currentUser.id===p.authorId) ? '' : '') + '
' + '
' + repliesHtml + (currentUser ? '
' : '

Log in to reply

') + '
' + '
'; }).join(''); } function toggleLike(pid) { if (!currentUser) { alert('Please log in to like posts.'); return; } window.FluenzyDB.likePost(pid, currentUser.id); renderFeed(); } function toggleReplies(pid) { var el = document.getElementById('replies-' + pid); if (el) el.classList.toggle('open'); } function submitReply(pid) { if (!currentUser) return; var inp = document.getElementById('ri-' + pid); var txt = inp ? inp.value.trim() : ''; if (!txt) return; window.FluenzyDB.replyPost(pid, { authorName: currentUser.name, authorId: currentUser.id, body: txt }); renderFeed(); setTimeout(function() { var el = document.getElementById('replies-' + pid); if (el) el.classList.add('open'); }, 50); } function sharePost(pid) { var url = window.location.href.split('#')[0] + '#post-' + pid; if (navigator.clipboard) { navigator.clipboard.writeText(url).then(function(){ alert('Link copied!'); }); } else { alert('Share: ' + url); } } function deletePost(pid) { if (!confirm('Delete this post?')) return; window.FluenzyDB.deletePost(pid); renderFeed(); updateCounts(); } /* ── SHOWCASE ── */ function renderShowcase() { var items = getShowcaseItems().slice(0, 3); var grid = document.getElementById('showcaseGrid'); if (!items.length) { grid.innerHTML = '

No showcases yet. Be first!

'; return; } grid.innerHTML = items.map(function(s) { return '
' + '
' + (COMM_ICONS[s.community]||'🎵') + '
' + '
' + esc(s.title) + '
' + esc(s.authorName) + '
' + '
'; }).join(''); } function renderFullShowcase() { var items = getShowcaseItems(); var grid = document.getElementById('fullShowcaseGrid'); var addBox = document.getElementById('addShowcaseBox'); if (currentUser && ['Guitar','Piano','Singing'].some(function(c){ return window.FluenzyDB.getBookings().some(function(b){ return (b.studentId===currentUser.id||b.teacherId===currentUser.id) && b.subject===c; }); })) addBox.style.display = 'block'; if (!items.length) { grid.innerHTML = '

No showcases yet. Music students — share your first recording!

'; return; } grid.innerHTML = items.map(function(s) { var liked = currentUser && (s.likedBy||[]).indexOf(currentUser.id)>=0; return '
' + '
' + (COMM_ICONS[s.community]||'🎵') + '
' + '
' + esc(s.title) + '
' + '
' + esc(s.authorName) + ' • ' + esc(s.community) + '
' + '
' + esc((s.desc||'').slice(0,60)) + (s.desc&&s.desc.length>60?'...':'') + '
' + '
' + '' + '' + '
' + '
'; }).join(''); } function getShowcaseItems() { return JSON.parse(localStorage.getItem('fluenzy_showcase')||'[]'); } function saveShowcaseItems(items) { localStorage.setItem('fluenzy_showcase', JSON.stringify(items)); } function submitShowcase() { if (!currentUser) { alert('Please log in.'); return; } var title = document.getElementById('scTitle').value.trim(); var link = document.getElementById('scLink').value.trim(); var desc = document.getElementById('scDesc').value.trim(); if (!title) { alert('Please add a title.'); return; } var bk = window.FluenzyDB.getBookings().find(function(b) { return (b.studentId===currentUser.id||b.teacherId===currentUser.id) && MUSIC_COMMS.indexOf(b.subject)>=0; }); var comm = bk ? bk.subject : 'Music'; var items = getShowcaseItems(); items.unshift({ id: Date.now().toString(36), title:title, link:link, desc:desc, community:comm, authorName:currentUser.name, authorId:currentUser.id, createdAt:new Date().toISOString(), likes:0, likedBy:[] }); saveShowcaseItems(items); document.getElementById('scTitle').value=''; document.getElementById('scLink').value=''; document.getElementById('scDesc').value=''; renderShowcase(); renderFullShowcase(); alert('Your showcase has been shared!'); } function openShowcase(url) { if (url) window.open(url, '_blank'); } function likeShowcase(id) { if (!currentUser) { alert('Log in to like.'); return; } var items = getShowcaseItems(); var s = items.find(function(x){ return x.id===id; }); if (!s) return; s.likedBy = s.likedBy||[]; if (s.likedBy.indexOf(currentUser.id)>=0) { s.likedBy=s.likedBy.filter(function(u){return u!==currentUser.id;}); s.likes=Math.max(0,(s.likes||0)-1); } else { s.likedBy.push(currentUser.id); s.likes=(s.likes||0)+1; } saveShowcaseItems(items); renderFullShowcase(); } function shareShowcase(id) { var url = window.location.href.split('?')[0] + '#showcase-section'; if (navigator.clipboard) navigator.clipboard.writeText(url).then(function(){ alert('Link copied!'); }); else alert('Share: ' + url); } function esc(s) { return String(s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); }