When user selects Reseptit category, recipe fields (time, servings, ingredients, steps) appear automatically. All other categories show the post body field instead. Removed the manual toggle buttons and cleaned up related CSS. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
657 lines
25 KiB
JavaScript
657 lines
25 KiB
JavaScript
// ===========================
|
|
// 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! ✓',
|
|
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.',
|
|
},
|
|
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! ✓',
|
|
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.',
|
|
}
|
|
};
|
|
|
|
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: [],
|
|
};
|
|
|
|
// ===========================
|
|
// CATEGORIES
|
|
// ===========================
|
|
function getCategoryLabel(catId) {
|
|
const cat = APP.categories.find(c => c.id === catId);
|
|
return cat ? cat.fi : catId;
|
|
}
|
|
|
|
function renderCategoryFilters() {
|
|
const cats = APP.categories;
|
|
const container = document.getElementById('categoryFilters');
|
|
if (!container) return;
|
|
container.innerHTML =
|
|
`<button class="filter-btn ${currentFilter === 'all' ? 'active' : ''}" onclick="setFilter('all',this)">${t('filter_all')}</button>` +
|
|
cats.map(c => `<button class="filter-btn ${currentFilter === c.id ? 'active' : ''}" onclick="setFilter('${c.id}',this)">${c.emoji ? c.emoji + ' ' : ''}${c.fi}</button>`).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;
|
|
});
|
|
}
|
|
|
|
// ===========================
|
|
// 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 `<p class="empty-note">${t('no_comments')}</p>`;
|
|
return comments.map(c => `
|
|
<div class="comment">
|
|
<div class="comment-header"><strong>${c.name}</strong><span>${c.time}</span></div>
|
|
<p>${c.text}</p>
|
|
</div>
|
|
`).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 ? `<div class="card-meta"><span>⏱ ${p.time}</span><span>👤 ${p.servings}</span></div>` : '';
|
|
const imgSrc = p.images?.[0];
|
|
const cardImgHTML = imgSrc
|
|
? `<div class="card-img card-has-photo"><img src="${imgSrc}" alt="${p.title}" /></div>`
|
|
: `<div class="card-img">${p.emoji}</div>`;
|
|
return `
|
|
<article class="recipe-card" data-category="${p.category}" data-subcategory="${Array.isArray(p.subcategory) ? p.subcategory.join(',') : (p.subcategory || '')}">
|
|
${cardImgHTML}
|
|
<div class="card-body">
|
|
<span class="category-tag">${getCategoryLabel(p.category)}</span>
|
|
<h3>${p.title}</h3>
|
|
${p.author ? `<p class="card-author">✍️ ${p.author}</p>` : ''}
|
|
<p>${p.desc || ''}</p>
|
|
${metaRow}
|
|
<div class="card-actions">
|
|
<button class="btn-outline" onclick="openPost('${p.id}')">${t('view_btn')}</button>
|
|
<div class="like-row">
|
|
<button class="like-btn ${liked ? 'liked' : ''}" data-like-id="${p.id}" onclick="toggleLike('${p.id}')">${liked ? t('liked_btn') : t('like_btn')}</button>
|
|
<span class="like-count" data-like-count="${p.id}">${likeCount}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
`;
|
|
}).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 || [];
|
|
if (!subs.length) { container.innerHTML = ''; return; }
|
|
container.innerHTML =
|
|
`<button class="sub-filter-btn ${currentSubFilter === 'all' ? 'active' : ''}" onclick="setSubFilter('all',this)">Kaikki</button>` +
|
|
subs.map(s => `<button class="sub-filter-btn ${currentSubFilter === s.id ? 'active' : ''}" onclick="setSubFilter('${s.id}',this)">${s.fi}</button>`).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 show = matchesCat && matchesSub && matchesSearch;
|
|
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
|
|
? `<ul>${p.ingredients.map(i => `<li>${i}</li>`).join('')}</ul>`
|
|
: `<p class="empty-note">${t('no_ingredients')}</p>`;
|
|
const stps = p.steps?.length
|
|
? `<ol>${p.steps.map(s => `<li>${s}</li>`).join('')}</ol>`
|
|
: `<p class="empty-note">${t('no_steps')}</p>`;
|
|
const copyBtn = p.ingredients?.length
|
|
? `<button class="copy-list-btn" onclick="copyIngredients('${p.id}')" style="margin-left:10px;padding:4px 12px;font-size:0.82rem;border:1px solid #ccc;background:#fff;border-radius:20px;cursor:pointer;">📋 Kopioi ostoslista</button>`
|
|
: '';
|
|
bodyHTML = `<div style="display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin-bottom:2px"><h3 style="margin:0">${t('modal_ingredients')}</h3>${copyBtn}</div>${ings}<h3>${t('modal_steps')}</h3>${stps}`;
|
|
} else {
|
|
bodyHTML = `<div class="post-body">${p.body || p.desc}</div>`;
|
|
}
|
|
|
|
const galleryHTML = p.images?.length
|
|
? `<div class="img-gallery">${p.images.map(src => `<img src="${src}" alt="${p.title}" />`).join('')}</div>`
|
|
: '';
|
|
|
|
document.getElementById('modalContent').innerHTML = `
|
|
${p.images?.length ? '' : `<span class="modal-emoji">${p.emoji}</span>`}
|
|
${galleryHTML}
|
|
<h2>${p.title}</h2>
|
|
<p class="modal-meta">
|
|
📂 ${getCategoryLabel(p.category)}
|
|
${p.time ? ` | ⏱ ${p.time} | 👤 ${p.servings}` : ''}
|
|
${p.author ? ` | ✍️ ${t('modal_by')} ${p.author}` : ''}
|
|
</p>
|
|
<div class="modal-like-row">
|
|
<button class="like-btn ${liked ? 'liked' : ''}" data-like-id="${p.id}" onclick="toggleLike('${p.id}')">${liked ? t('liked_btn') : t('like_btn')}</button>
|
|
<span class="like-count" data-like-count="${p.id}">${likeCount}</span>
|
|
</div>
|
|
${bodyHTML}
|
|
<div class="comments-section">
|
|
<h3 id="comments-heading-${p.id}">${t('modal_comments')} (${comments.length})</h3>
|
|
<div id="comments-list-${p.id}">${renderCommentsList(comments)}</div>
|
|
<form class="comment-form" onsubmit="submitComment(event,'${p.id}')">
|
|
<input type="text" placeholder="${t('comment_name_ph')}" required maxlength="50" />
|
|
<textarea placeholder="${t('comment_ph')}" rows="3" required maxlength="500"></textarea>
|
|
<!-- honeypot: bots fill this, humans don't see it -->
|
|
<input type="text" name="website" tabindex="-1" autocomplete="off" style="display:none" />
|
|
<div class="captcha-row">
|
|
<label class="captcha-label" id="captcha-question"></label>
|
|
<input type="number" id="captcha-input" placeholder="Vastaus" required min="0" max="99" style="width:100px" />
|
|
</div>
|
|
<button type="submit" class="btn-outline">${t('comment_btn')}</button>
|
|
</form>
|
|
</div>
|
|
`;
|
|
|
|
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 = '';
|
|
}
|
|
|
|
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 = `<option value="">— Valitse alikategoria —</option>` +
|
|
subs.map(s => `<option value="${s.id}">${s.fi}</option>`).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 => `<option value="${c.id}">${c.emoji || ''} ${c.fi}</option>`).join('');
|
|
updateSubcategoryPicker(); // asettaa myös oikean tyypin kategorian mukaan
|
|
document.getElementById('submitOverlay').classList.add('open');
|
|
document.body.style.overflow = 'hidden';
|
|
generateSubmitCaptcha();
|
|
}
|
|
|
|
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
|
|
// ===========================
|
|
function handleSubmit(e) {
|
|
e.preventDefault();
|
|
const btn = e.target.querySelector('button[type="submit"]');
|
|
btn.textContent = t('msg_sent');
|
|
btn.style.background = '#5c8a4a';
|
|
btn.disabled = true;
|
|
e.target.reset();
|
|
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 { return JSON.parse(localStorage.getItem('tykkaafi_likes') || '[]'); } catch { return []; }
|
|
}
|
|
function saveLocalLikes(arr) {
|
|
try { localStorage.setItem('tykkaafi_likes', JSON.stringify(arr)); } catch {}
|
|
}
|
|
|
|
// ===========================
|
|
async function init() {
|
|
try {
|
|
const [postsData, catsData, likesData] = await Promise.all([
|
|
apiGet('posts'),
|
|
apiGet('categories'),
|
|
apiGet('likes'),
|
|
]);
|
|
APP.posts = postsData.posts || [];
|
|
APP.categories = catsData.categories || [];
|
|
APP.likes = likesData.likes || {};
|
|
// Merge server session likes with localStorage — remember likes across sessions
|
|
const serverLikes = likesData.userLikes || [];
|
|
const localLikes = getLocalLikes();
|
|
APP.userLikes = [...new Set([...serverLikes, ...localLikes])];
|
|
} catch (e) {
|
|
console.error('API virhe:', e);
|
|
}
|
|
applyTranslations();
|
|
renderCategoryFilters();
|
|
renderSubFilters();
|
|
renderCards();
|
|
}
|
|
|
|
init();
|