// =========================== // TRANSLATIONS // =========================== const T = { fi: { tagline: 'Jaa asioita joista tykkäät — reseptejä, vinkkejä, ideoita', nav_posts: 'Julkaisut', nav_about: 'Tietoa', nav_contact: 'Yhteystiedot', nav_admin: 'Hallinta', hero_title: 'Tervetuloa tykkää.fi:hin!', hero_desc: 'Jaa ja löydä reseptejä, neulomisvinkkejä, elämänohjeita — ja kaikkea muuta mitä rakastat.', hero_btn: 'Selaa julkaisuja', search_ph: 'Hae julkaisuja...', filter_all: 'Kaikki', view_btn: 'Lue lisää', no_results: 'Julkaisuja ei löydy. Kokeile eri hakusanaa!', about_title: 'Mikä tykkää.fi?', about_text: 'tykkää.fi on avoin yhteisö, jonne kuka tahansa voi tulla jakamaan asioita joista tykkää. Reseptejä, neulomisohjeita, käsityöideoita tai ihan mitä muuta kivaa — tärkeintä on jakamisen ilo. Lisää oma julkaisusi yläkulman napista!', contact_title: 'Ota yhteyttä', contact_desc: 'Kysyttävää tai ehdotuksia? Lähetä meille viestiä!', name_ph: 'Nimesi', email_ph: 'Sähköpostisi', msg_ph: 'Viestisi...', send_btn: 'Lähetä viesti', msg_sent: 'Viesti lähetetty! ✓', msg_error: 'Virhe lähetyksessä. Yritä uudelleen.', footer: 'Avoin yhteisö kaikille', modal_by: 'Kirjoittanut', modal_ingredients: 'Ainekset', modal_steps: 'Ohjeet', modal_comments: 'Kommentit', no_comments: 'Ei kommentteja vielä. Ole ensimmäinen!', comment_name_ph: 'Nimesi', comment_ph: 'Kirjoita kommentti...', comment_btn: 'Lähetä kommentti', like_btn: '❤️ Tykkää', liked_btn: '❤️ Tykätty', no_ingredients: 'Aineksia ei lisätty.', no_steps: 'Ohjeita ei lisätty.', login_btn: 'Kirjaudu', logout_btn: 'Kirjaudu ulos', register_btn: 'Rekisteröidy', login_title: 'Kirjaudu sisään', register_title: 'Luo tili', nickname_ph: 'Nimimerkki', password_ph: 'Salasana', email_ph_reg: 'Sähköposti (vapaaehtoinen)', login_submit: 'Kirjaudu', register_submit: 'Luo tili', switch_to_login: 'Onko sinulla jo tili? Kirjaudu sisään', switch_to_register: 'Eikö sinulla ole tiliä? Rekisteröidy', logged_in_as: 'Kirjautunut:', my_likes_btn: '❤️ Tykkäämäni', auth_fields_required: 'Täytä nimimerkki ja salasana.', }, en: { tagline: 'Share things you love — recipes, tips, ideas', nav_posts: 'Posts', nav_about: 'About', nav_contact: 'Contact', nav_admin: 'Admin', hero_title: 'Welcome to tykkää.fi!', hero_desc: 'Share and discover recipes, knitting tips, life advice — and everything else you love.', hero_btn: 'Browse Posts', search_ph: 'Search posts...', filter_all: 'All', view_btn: 'Read more', no_results: 'No posts found. Try a different search!', about_title: 'What is tykkää.fi?', about_text: 'tykkää.fi is an open community where anyone can share things they love. Recipes, knitting patterns, craft ideas or anything fun — the joy of sharing is what matters. Add your own post using the button in the top corner!', contact_title: 'Get in Touch', contact_desc: 'Questions or suggestions? Send us a message!', name_ph: 'Your name', email_ph: 'Your email', msg_ph: 'Your message...', send_btn: 'Send Message', msg_sent: 'Message Sent! ✓', msg_error: 'Error sending. Please try again.', footer: 'Open community for everyone', modal_by: 'By', modal_ingredients: 'Ingredients', modal_steps: 'Instructions', modal_comments: 'Comments', no_comments: 'No comments yet. Be the first!', comment_name_ph: 'Your name', comment_ph: 'Write a comment...', comment_btn: 'Post Comment', like_btn: '❤️ Like', liked_btn: '❤️ Liked', no_ingredients: 'No ingredients listed.', no_steps: 'No instructions listed.', login_btn: 'Log in', logout_btn: 'Log out', register_btn: 'Register', login_title: 'Log In', register_title: 'Create Account', nickname_ph: 'Nickname', password_ph: 'Password', email_ph_reg: 'Email (optional)', login_submit: 'Log In', register_submit: 'Create Account', switch_to_login: 'Already have an account? Log in', switch_to_register: "Don't have an account? Register", logged_in_as: 'Logged in:', my_likes_btn: '❤️ My Likes', auth_fields_required: 'Nickname and password are required.', } }; const lang = 'fi'; function t(key) { return (T[lang] && T[lang][key]) ? T[lang][key] : key; } function applyTranslations() { document.documentElement.lang = lang; document.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.dataset.i18n); }); document.querySelectorAll('[data-i18n-ph]').forEach(el => { el.placeholder = t(el.dataset.i18nPh); }); } // =========================== // API // =========================== async function apiGet(action, params = {}) { const qs = new URLSearchParams({ action, ...params }).toString(); const res = await fetch(`api.php?${qs}`); return res.json(); } async function apiPost(action, body = {}) { const res = await fetch(`api.php?action=${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); return res.json(); } // =========================== // APP STATE // =========================== const APP = { posts: [], categories: [], likes: {}, userLikes: [], user: null, showOnlyLiked: false, }; // =========================== // CATEGORIES // =========================== function getCategoryLabel(catId) { const cat = APP.categories.find(c => c.id === catId); return cat ? cat.fi : catId; } function countPostsInCategory(catId) { return APP.posts.filter(p => p.category === catId).length; } function countPostsInSubcategory(catId, subId) { return APP.posts.filter(p => { if (p.category !== catId) return false; const subs = Array.isArray(p.subcategory) ? p.subcategory : [p.subcategory].filter(Boolean); return subs.includes(subId); }).length; } function renderCategoryFilters() { const cats = APP.categories.filter(c => countPostsInCategory(c.id) > 0); const container = document.getElementById('categoryFilters'); if (!container) return; container.innerHTML = `` + cats.map(c => ``).join(''); } // =========================== // LIKES // =========================== async function toggleLike(id) { const data = await apiPost('toggle_like', { postId: id }); if (data.error) return; const liked = data.liked; const count = data.count; // Update in-memory state APP.likes[id] = count; if (liked) { if (!APP.userLikes.includes(id)) APP.userLikes.push(id); } else { APP.userLikes = APP.userLikes.filter(x => x !== id); } // Persist to localStorage so likes survive session expiry saveLocalLikes(APP.userLikes); // Update all like buttons for this post (card + modal) document.querySelectorAll(`[data-like-id="${id}"]`).forEach(btn => { btn.textContent = liked ? t('liked_btn') : t('like_btn'); btn.classList.toggle('liked', liked); }); document.querySelectorAll(`[data-like-count="${id}"]`).forEach(el => { el.textContent = count; }); if (APP.showOnlyLiked) filterPosts(); } // =========================== // COMMENTS + SPAM PROTECTION // =========================== let captchaAnswer = 0; function generateCaptcha() { const a = Math.floor(Math.random() * 9) + 1; const b = Math.floor(Math.random() * 9) + 1; captchaAnswer = a + b; return `Laske: ${a} + ${b} = ?`; } function renderCommentsList(comments) { if (!comments || !comments.length) return `

${t('no_comments')}

`; return comments.map(c => `
${c.name}${c.time}

${c.text}

`).join(''); } // =========================== // RENDER CARDS // =========================== function renderCards() { const grid = document.getElementById('postGrid'); if (!grid) return; grid.innerHTML = APP.posts.map(p => { const liked = APP.userLikes.includes(p.id); const likeCount = APP.likes[p.id] || 0; const metaRow = p.time ? `
⏱ ${p.time}👤 ${p.servings}
` : ''; const imgSrc = p.images?.[0]; const cardImgHTML = imgSrc ? `
${p.title}
` : `
${p.emoji}
`; return `
${cardImgHTML}
${getCategoryLabel(p.category)}

${p.title}

${p.author ? `

✍️ ${p.author}

` : ''}

${p.desc || ''}

${metaRow}
`; }).join(''); } // =========================== // FILTER & SEARCH // =========================== let currentFilter = 'all'; let currentSubFilter = 'all'; function setFilter(category, btn) { currentFilter = category; currentSubFilter = 'all'; document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); renderSubFilters(); filterPosts(); } function setSubFilter(sub, btn) { currentSubFilter = sub; document.querySelectorAll('.sub-filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); filterPosts(); } function renderSubFilters() { const container = document.getElementById('subCategoryFilters'); if (!container) return; if (currentFilter === 'all') { container.innerHTML = ''; return; } const cat = APP.categories.find(c => c.id === currentFilter); const subs = (cat?.subcategories || []).filter(s => countPostsInSubcategory(currentFilter, s.id) > 0); if (!subs.length) { container.innerHTML = ''; return; } container.innerHTML = `` + subs.map(s => ``).join(''); } function filterPosts() { const query = (document.getElementById('search')?.value || '').toLowerCase(); const cards = document.querySelectorAll('.recipe-card'); let visible = 0; cards.forEach(card => { const matchesCat = currentFilter === 'all' || card.dataset.category === currentFilter; const postSubs = (card.dataset.subcategory || '').split(',').filter(Boolean); const matchesSub = currentSubFilter === 'all' || postSubs.includes(currentSubFilter); const title = (card.querySelector('h3')?.textContent || '').toLowerCase(); const desc = (card.querySelector('p:not(.card-author)')?.textContent || '').toLowerCase(); const subLbl = (card.dataset.subcategory || '').toLowerCase(); const matchesSearch = !query || title.includes(query) || desc.includes(query) || subLbl.includes(query); const postId = card.querySelector('[data-like-id]')?.dataset.likeId || ''; const matchesLikes = !APP.showOnlyLiked || APP.userLikes.includes(postId); const show = matchesCat && matchesSub && matchesSearch && matchesLikes; card.style.display = show ? '' : 'none'; if (show) visible++; }); const noRes = document.getElementById('noResults'); if (noRes) noRes.style.display = visible === 0 ? 'block' : 'none'; } // =========================== // MODAL // =========================== async function openPost(id) { const p = APP.posts.find(x => x.id === id); if (!p) return; const liked = APP.userLikes.includes(p.id); const likeCount = APP.likes[p.id] || 0; // Load comments from server const commData = await apiGet('comments', { postId: id }); const comments = commData.comments || []; let bodyHTML = ''; if (p.type === 'recipe') { const ings = p.ingredients?.length ? `` : `

${t('no_ingredients')}

`; const stps = p.steps?.length ? `
    ${p.steps.map(s => `
  1. ${s}
  2. `).join('')}
` : `

${t('no_steps')}

`; const copyBtn = p.ingredients?.length ? `` : ''; bodyHTML = `

${t('modal_ingredients')}

${copyBtn}
${ings}

${t('modal_steps')}

${stps}`; } else { bodyHTML = `
${p.body || p.desc}
`; } const galleryHTML = p.images?.length ? `` : ''; document.getElementById('modalContent').innerHTML = ` ${p.images?.length ? '' : `${p.emoji}`} ${galleryHTML}

${p.title}

${bodyHTML}

${t('modal_comments')} (${comments.length})

${renderCommentsList(comments)}
`; document.getElementById('modalOverlay').classList.add('open'); document.body.style.overflow = 'hidden'; const qEl = document.getElementById('captcha-question'); if (qEl) qEl.textContent = generateCaptcha(); } async function submitComment(e, postId) { e.preventDefault(); // Honeypot check if (e.target.querySelector('[name="website"]').value) return; const name = e.target.querySelector('input[type="text"]').value.trim(); const text = e.target.querySelector('textarea').value.trim(); if (!name || !text) return; // CAPTCHA check const captchaInput = parseInt(e.target.querySelector('#captcha-input').value, 10); if (captchaInput !== captchaAnswer) { const qEl = document.getElementById('captcha-question'); if (qEl) { qEl.style.color = '#c04040'; qEl.textContent = 'Väärä vastaus! ' + generateCaptcha(); setTimeout(() => { qEl.style.color = ''; }, 2000); } e.target.querySelector('#captcha-input').value = ''; return; } const btn = e.target.querySelector('button[type="submit"]'); btn.disabled = true; btn.textContent = '...'; const data = await apiPost('add_comment', { postId, name, text }); btn.disabled = false; btn.textContent = t('comment_btn'); if (data.error) { const qEl = document.getElementById('captcha-question'); if (qEl) { qEl.style.color = '#c04040'; qEl.textContent = data.error; } return; } e.target.reset(); const qEl = document.getElementById('captcha-question'); if (qEl) qEl.textContent = generateCaptcha(); // Refresh comments const commData = await apiGet('comments', { postId }); const comments = commData.comments || []; const listEl = document.getElementById(`comments-list-${postId}`); const headEl = document.getElementById(`comments-heading-${postId}`); if (listEl) listEl.innerHTML = renderCommentsList(comments); if (headEl) headEl.textContent = `${t('modal_comments')} (${comments.length})`; } function closeModal() { document.getElementById('modalOverlay').classList.remove('open'); document.body.style.overflow = ''; } // =========================== // LIGHTBOX // =========================== function openLightbox(src) { document.getElementById('lightboxImg').src = src; document.getElementById('lightboxOverlay').classList.add('open'); } function closeLightbox() { document.getElementById('lightboxOverlay').classList.remove('open'); document.getElementById('lightboxImg').src = ''; } document.addEventListener('keydown', e => { if (e.key === 'Escape') closeLightbox(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); }); // =========================== // PUBLIC SUBMISSION // =========================== let submitCaptchaAnswer = 0; let submitType = 'recipe'; function generateSubmitCaptcha() { const a = Math.floor(Math.random() * 9) + 1; const b = Math.floor(Math.random() * 9) + 1; submitCaptchaAnswer = a + b; const qEl = document.getElementById('sub-captcha-question'); if (qEl) qEl.textContent = `Laske: ${a} + ${b} = ?`; } function setSubmitType(type) { submitType = type; document.getElementById('sub-recipeFields').style.display = type === 'recipe' ? '' : 'none'; document.getElementById('sub-postFields').style.display = type === 'post' ? '' : 'none'; } function updateSubcategoryPicker() { const catId = document.getElementById('sub-category').value; const cat = APP.categories.find(c => c.id === catId); const subs = cat?.subcategories || []; const group = document.getElementById('sub-subcategory-group'); const sel = document.getElementById('sub-subcategory'); if (!subs.length) { group.style.display = 'none'; sel.innerHTML = ''; } else { sel.innerHTML = `` + subs.map(s => ``).join(''); group.style.display = ''; } // Näytä reseptikentät jos kategoria on Reseptit, muuten julkaisukentät setSubmitType(catId === 'recipes' ? 'recipe' : 'post'); } function openSubmitModal() { const cats = APP.categories; const sel = document.getElementById('sub-category'); sel.innerHTML = cats.map(c => ``).join(''); updateSubcategoryPicker(); // asettaa myös oikean tyypin kategorian mukaan document.getElementById('submitOverlay').classList.add('open'); document.body.style.overflow = 'hidden'; generateSubmitCaptcha(); if (APP.user) { const authorField = document.getElementById('sub-author'); if (authorField && !authorField.value) authorField.value = APP.user.nickname; } } function closeSubmitModal() { document.getElementById('submitOverlay').classList.remove('open'); document.body.style.overflow = ''; } async function resizeImage(file, maxPx = 1920, quality = 0.85) { // Älä muuta GIF:ejä (animaatio menisi rikki) if (file.type === 'image/gif') return file; return new Promise(resolve => { const img = new Image(); const url = URL.createObjectURL(file); img.onload = () => { URL.revokeObjectURL(url); const ratio = Math.min(maxPx / img.naturalWidth, maxPx / img.naturalHeight, 1); // Jos kuva on jo tarpeeksi pieni, palauta sellaisenaan if (ratio === 1 && file.size < 1.5 * 1024 * 1024) { resolve(file); return; } const canvas = document.createElement('canvas'); canvas.width = Math.round(img.naturalWidth * ratio); canvas.height = Math.round(img.naturalHeight * ratio); canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height); canvas.toBlob( blob => resolve(new File([blob], 'image.jpg', { type: 'image/jpeg' })), 'image/jpeg', quality ); }; img.src = url; }); } async function uploadImg(input, hiddenId, previewId, labelId) { const file = input.files[0]; if (!file) return; const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; if (!allowed.includes(file.type)) { alert('Tuetut kuvamuodot: JPG, PNG, GIF, WebP'); input.value = ''; return; } const lbl = document.getElementById(labelId); lbl.style.opacity = '0.5'; let resized; try { resized = await resizeImage(file); } catch (e) { lbl.style.opacity = ''; alert('Kuvan käsittely epäonnistui: ' + e.message); return; } const formData = new FormData(); formData.append('file', resized, resized.name || 'image.jpg'); try { const res = await fetch('upload.php', { method: 'POST', body: formData }); const text = await res.text(); let data; try { data = JSON.parse(text); } catch(e) { throw new Error('Palvelin vastasi (HTTP ' + res.status + '): ' + (text.slice(0, 200) || '(tyhjä)')); } if (data.error) throw new Error(data.error); document.getElementById(hiddenId).value = data.url; const prev = document.getElementById(previewId); prev.src = data.url; prev.style.display = 'block'; lbl.classList.add('has-image'); } catch (e) { alert('Kuvan lataus epäonnistui: ' + e.message); } finally { lbl.style.opacity = ''; } } function showSubError(msg) { const el = document.getElementById('sub-error'); el.textContent = msg; el.style.display = 'block'; } async function submitPublicPost() { document.getElementById('sub-error').style.display = 'none'; // Honeypot if (document.getElementById('sub-honeypot').value) return; const title = document.getElementById('sub-title').value.trim(); if (!title) { showSubError('Anna julkaisulle otsikko.'); return; } if (!document.getElementById('sub-img1').value) { showSubError('Lisää vähintään 1 kuva julkaisuun.'); return; } // CAPTCHA const captchaVal = parseInt(document.getElementById('sub-captcha-input').value, 10); if (captchaVal !== submitCaptchaAnswer) { showSubError('Väärä vastaus captchaan!'); document.getElementById('sub-captcha-input').value = ''; generateSubmitCaptcha(); return; } const category = document.getElementById('sub-category').value; const subVal = document.getElementById('sub-subcategory')?.value || ''; const subcategory = subVal ? [subVal] : []; const cat = APP.categories.find(c => c.id === category); const emoji = cat?.emoji || '📝'; const author = document.getElementById('sub-author').value.trim() || 'Vieras'; const desc = document.getElementById('sub-desc').value.trim(); const images = [ document.getElementById('sub-img1').value.trim(), document.getElementById('sub-img2').value.trim(), document.getElementById('sub-img3').value.trim(), ].filter(Boolean); const post = { id: title.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/\s+/g,'_').replace(/[^a-z0-9_]/g,'') + '_' + Date.now(), title, emoji, category, subcategory, author, desc, images, type: submitType, }; if (submitType === 'recipe') { post.time = document.getElementById('sub-time').value.trim(); post.servings = document.getElementById('sub-servings').value.trim(); post.ingredients = document.getElementById('sub-ingredients').value.split('\n').map(s => s.trim()).filter(Boolean); post.steps = document.getElementById('sub-steps').value.split('\n').map(s => s.trim()).filter(Boolean); } else { post.body = document.getElementById('sub-body').value.trim(); } const btn = document.querySelector('.submit-btn'); btn.disabled = true; btn.textContent = 'Lähetetään...'; const data = await apiPost('add_post', { post }); btn.disabled = false; btn.textContent = '📨 Lähetä julkaisu'; if (data.error) { showSubError(data.error); return; } // Update local state APP.posts.push(post); closeSubmitModal(); renderCards(); renderCategoryFilters(); // Reset form fields ['sub-title','sub-author','sub-desc','sub-img1','sub-img2','sub-img3', 'sub-time','sub-servings','sub-ingredients','sub-steps','sub-body'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; }); [1,2,3].forEach(i => { const prev = document.getElementById(`sub-prev${i}`); const lbl = document.getElementById(`sub-lbl${i}`); if (prev) { prev.src = ''; prev.style.display = 'none'; } if (lbl) lbl.classList.remove('has-image'); }); document.getElementById('posts')?.scrollIntoView({ behavior: 'smooth' }); } // =========================== // CONTACT FORM // =========================== // =========================== // CONTACT FORM // =========================== async function handleSubmit(e) { e.preventDefault(); const btn = e.target.querySelector('button[type="submit"]'); const name = document.getElementById('contact-name').value.trim(); const email = document.getElementById('contact-email').value.trim(); const message = document.getElementById('contact-msg').value.trim(); btn.disabled = true; try { await apiPost('contact', { name, email, message }); btn.textContent = t('msg_sent'); btn.style.background = '#5c8a4a'; e.target.reset(); setTimeout(() => { btn.textContent = t('send_btn'); btn.style.background = ''; btn.disabled = false; }, 4000); } catch { btn.textContent = t('msg_error'); btn.style.background = '#c04040'; setTimeout(() => { btn.textContent = t('send_btn'); btn.style.background = ''; btn.disabled = false; }, 4000); } } // =========================== // INIT // =========================== // COPY INGREDIENTS // =========================== function copyIngredients(postId) { const p = APP.posts.find(x => x.id === postId); if (!p?.ingredients?.length) return; const text = p.title + '\n\nOstoslista:\n' + p.ingredients.map(i => '• ' + i).join('\n'); navigator.clipboard.writeText(text).then(() => { const btn = document.querySelector('.copy-list-btn'); if (btn) { btn.textContent = '✅ Kopioitu!'; setTimeout(() => { btn.textContent = '📋 Kopioi ostoslista'; }, 2000); } }).catch(() => {}); } // =========================== // LOCAL LIKES PERSISTENCE // =========================== function getLocalLikes() { try { const v = JSON.parse(localStorage.getItem('tykkaafi_likes') || '[]'); return Array.isArray(v) ? v : []; } catch { return []; } } function saveLocalLikes(arr) { try { localStorage.setItem('tykkaafi_likes', JSON.stringify(arr)); } catch {} } // =========================== // AUTH // =========================== let authMode = 'login'; let authCaptchaAnswer = 0; function generateAuthCaptcha() { const a = Math.floor(Math.random() * 9) + 1; const b = Math.floor(Math.random() * 9) + 1; authCaptchaAnswer = a + b; const qEl = document.getElementById('auth-captcha-question'); if (qEl) qEl.textContent = `Laske: ${a} + ${b} = ?`; } function openAuthModal(mode = 'login') { authMode = mode; renderAuthForm(); document.getElementById('authOverlay').classList.add('open'); document.body.style.overflow = 'hidden'; if (mode === 'register') generateAuthCaptcha(); } function closeAuthModal() { document.getElementById('authOverlay').classList.remove('open'); document.body.style.overflow = ''; } function renderAuthForm() { const isLogin = authMode === 'login'; document.getElementById('authContent').innerHTML = `

${isLogin ? t('login_title') : t('register_title')}

${!isLogin ? `
` : ''}
${!isLogin ? `
` : ''}

${isLogin ? t('switch_to_register') : t('switch_to_login')}

`; } async function submitAuth() { const errEl = document.getElementById('auth-error'); errEl.textContent = ''; errEl.style.display = 'none'; const nickname = document.getElementById('auth-nickname').value.trim(); const password = document.getElementById('auth-password').value; const emailEl = document.getElementById('auth-email'); const email = emailEl ? emailEl.value.trim() : ''; if (!nickname || !password) { errEl.textContent = t('auth_fields_required'); errEl.style.display = 'block'; return; } if (authMode === 'register') { // Honeypot const hp = document.getElementById('auth-honeypot'); if (hp && hp.value) return; // CAPTCHA const captchaVal = parseInt(document.getElementById('auth-captcha-input').value, 10); if (captchaVal !== authCaptchaAnswer) { errEl.textContent = 'Väärä vastaus captchaan!'; errEl.style.display = 'block'; document.getElementById('auth-captcha-input').value = ''; generateAuthCaptcha(); return; } } const action = authMode === 'login' ? 'user_login' : 'user_register'; const body = { nickname, password }; if (authMode === 'register') body.email = email; const data = await apiPost(action, body); if (data.error) { errEl.textContent = data.error; errEl.style.display = 'block'; return; } APP.user = data.user; if (data.user.likes?.length) { APP.userLikes = [...new Set([...APP.userLikes, ...data.user.likes])]; saveLocalLikes(APP.userLikes); } closeAuthModal(); updateUserUI(); renderCards(); } async function logoutUser() { await apiPost('user_logout'); APP.user = null; APP.showOnlyLiked = false; updateUserUI(); renderCards(); filterPosts(); } function updateUserUI() { const container = document.getElementById('userArea'); if (!container) return; if (APP.user) { container.innerHTML = ` ${t('logged_in_as')} ${APP.user.nickname} `; } else { container.innerHTML = ` `; } updateLikesFilterBtn(); } // =========================== // LIKES FILTER // =========================== function updateLikesFilterBtn() { const btn = document.getElementById('likesFilterBtn'); if (!btn) return; btn.textContent = APP.showOnlyLiked ? ('❤️ ' + t('my_likes_btn').replace('❤️ ', '') + ' ✓') : t('my_likes_btn'); btn.classList.toggle('active', APP.showOnlyLiked); } function toggleLikesFilter() { APP.showOnlyLiked = !APP.showOnlyLiked; updateLikesFilterBtn(); filterPosts(); } // =========================== async function init() { try { const [postsData, catsData, likesData, userData] = await Promise.all([ apiGet('posts'), apiGet('categories'), apiGet('likes'), apiGet('user_check'), ]); APP.posts = postsData.posts || []; APP.categories = catsData.categories || []; APP.likes = likesData.likes || {}; if (userData.loggedIn && userData.user) { APP.user = userData.user; const serverLikes = Array.isArray(userData.user.likes) ? userData.user.likes : []; const localLikes = getLocalLikes(); APP.userLikes = [...new Set([...serverLikes, ...localLikes])]; } else { const serverLikes = Array.isArray(likesData.userLikes) ? likesData.userLikes : []; const localLikes = getLocalLikes(); APP.userLikes = [...new Set([...serverLikes, ...localLikes])]; } } catch (e) { console.error('API virhe:', e); } applyTranslations(); updateUserUI(); renderCategoryFilters(); renderSubFilters(); renderCards(); } init();