Add user registration/login, persistent likes, category hiding, and contact email
- User auth: register (nickname + password + email), login, logout with PHP sessions - Persistent likes: logged-in users' likes saved to users.json, anonymous via session - "Tykkäämäni" filter button next to search — filter liked posts, combinable with search - Hide empty/sparse categories from filter buttons until posts exist - Replace broken contact form with simple mailto link (info@tykkaa.fi) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
211
script.js
211
script.js
@@ -15,9 +15,7 @@ const T = {
|
||||
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! ✓',
|
||||
contact_desc: 'Kysyttävää tai ehdotuksia? Lähetä meille sähköpostia!',
|
||||
footer: 'Avoin yhteisö kaikille',
|
||||
modal_by: 'Kirjoittanut',
|
||||
modal_ingredients: 'Ainekset', modal_steps: 'Ohjeet',
|
||||
@@ -27,6 +25,14 @@ const T = {
|
||||
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',
|
||||
@@ -41,9 +47,7 @@ const T = {
|
||||
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! ✓',
|
||||
contact_desc: 'Questions or suggestions? Send us an email!',
|
||||
footer: 'Open community for everyone',
|
||||
modal_by: 'By',
|
||||
modal_ingredients: 'Ingredients', modal_steps: 'Instructions',
|
||||
@@ -53,6 +57,14 @@ const T = {
|
||||
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.',
|
||||
}
|
||||
};
|
||||
|
||||
@@ -91,6 +103,8 @@ const APP = {
|
||||
categories: [],
|
||||
likes: {},
|
||||
userLikes: [],
|
||||
user: null,
|
||||
showOnlyLiked: false,
|
||||
};
|
||||
|
||||
// ===========================
|
||||
@@ -101,8 +115,20 @@ function getCategoryLabel(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;
|
||||
const cats = APP.categories.filter(c => countPostsInCategory(c.id) > 0);
|
||||
const container = document.getElementById('categoryFilters');
|
||||
if (!container) return;
|
||||
container.innerHTML =
|
||||
@@ -138,6 +164,7 @@ async function toggleLike(id) {
|
||||
document.querySelectorAll(`[data-like-count="${id}"]`).forEach(el => {
|
||||
el.textContent = count;
|
||||
});
|
||||
if (APP.showOnlyLiked) filterPosts();
|
||||
}
|
||||
|
||||
// ===========================
|
||||
@@ -225,7 +252,7 @@ function renderSubFilters() {
|
||||
if (!container) return;
|
||||
if (currentFilter === 'all') { container.innerHTML = ''; return; }
|
||||
const cat = APP.categories.find(c => c.id === currentFilter);
|
||||
const subs = cat?.subcategories || [];
|
||||
const subs = (cat?.subcategories || []).filter(s => countPostsInSubcategory(currentFilter, s.id) > 0);
|
||||
if (!subs.length) { container.innerHTML = ''; return; }
|
||||
container.innerHTML =
|
||||
`<button class="sub-filter-btn ${currentSubFilter === 'all' ? 'active' : ''}" onclick="setSubFilter('all',this)">Kaikki</button>` +
|
||||
@@ -244,7 +271,9 @@ function filterPosts() {
|
||||
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;
|
||||
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++;
|
||||
});
|
||||
@@ -443,6 +472,10 @@ function openSubmitModal() {
|
||||
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() {
|
||||
@@ -605,20 +638,6 @@ async function submitPublicPost() {
|
||||
// ===========================
|
||||
// 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
|
||||
// ===========================
|
||||
@@ -638,31 +657,165 @@ function copyIngredients(postId) {
|
||||
// LOCAL LIKES PERSISTENCE
|
||||
// ===========================
|
||||
function getLocalLikes() {
|
||||
try { return JSON.parse(localStorage.getItem('tykkaafi_likes') || '[]'); } catch { return []; }
|
||||
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';
|
||||
|
||||
function openAuthModal(mode = 'login') {
|
||||
authMode = mode;
|
||||
renderAuthForm();
|
||||
document.getElementById('authOverlay').classList.add('open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeAuthModal() {
|
||||
document.getElementById('authOverlay').classList.remove('open');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function renderAuthForm() {
|
||||
const isLogin = authMode === 'login';
|
||||
document.getElementById('authContent').innerHTML = `
|
||||
<h2>${isLogin ? t('login_title') : t('register_title')}</h2>
|
||||
<div class="form-group">
|
||||
<label>${t('nickname_ph')}</label>
|
||||
<input type="text" id="auth-nickname" placeholder="${t('nickname_ph')}" maxlength="30" />
|
||||
</div>
|
||||
${!isLogin ? `<div class="form-group">
|
||||
<label>${t('email_ph_reg')}</label>
|
||||
<input type="email" id="auth-email" placeholder="${t('email_ph_reg')}" maxlength="100" />
|
||||
</div>` : ''}
|
||||
<div class="form-group">
|
||||
<label>${t('password_ph')}</label>
|
||||
<input type="password" id="auth-password" placeholder="${t('password_ph')}" maxlength="100" />
|
||||
</div>
|
||||
<p class="auth-error" id="auth-error"></p>
|
||||
<button class="submit-btn" onclick="submitAuth()">${isLogin ? t('login_submit') : t('register_submit')}</button>
|
||||
<p class="auth-switch" onclick="openAuthModal('${isLogin ? 'register' : 'login'}')">
|
||||
${isLogin ? t('switch_to_register') : t('switch_to_login')}
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<span class="user-greeting">${t('logged_in_as')} <strong>${APP.user.nickname}</strong></span>
|
||||
<button class="user-btn logout-btn" onclick="logoutUser()">${t('logout_btn')}</button>
|
||||
`;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<button class="user-btn login-btn" onclick="openAuthModal('login')">${t('login_btn')}</button>
|
||||
<button class="user-btn register-btn" onclick="openAuthModal('register')">${t('register_btn')}</button>
|
||||
`;
|
||||
}
|
||||
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] = await Promise.all([
|
||||
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 || {};
|
||||
// Merge server session likes with localStorage — remember likes across sessions
|
||||
const serverLikes = likesData.userLikes || [];
|
||||
const localLikes = getLocalLikes();
|
||||
APP.userLikes = [...new Set([...serverLikes, ...localLikes])];
|
||||
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user