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:
134
api.php
134
api.php
@@ -52,6 +52,19 @@ function isAdmin(): bool {
|
||||
return !empty($_SESSION['tykkaafi_admin']);
|
||||
}
|
||||
|
||||
function isLoggedIn(): bool {
|
||||
return !empty($_SESSION['tykkaafi_user_id']);
|
||||
}
|
||||
|
||||
function getLoggedInUser(): ?array {
|
||||
if (!isLoggedIn()) return null;
|
||||
$users = readData('users.json', []);
|
||||
foreach ($users as $u) {
|
||||
if ($u['id'] === $_SESSION['tykkaafi_user_id']) return $u;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Oletusdata ────────────────────────────────────────────────
|
||||
// Kategoriat jotka on poistettu — poistetaan automaattisesti live-serveriltä
|
||||
const REMOVED_CATEGORIES = ['knitting', 'kasvit', 'matkustus', 'tips'];
|
||||
@@ -691,26 +704,55 @@ switch ($action) {
|
||||
ok(['categories' => getOrInitCategories()]);
|
||||
|
||||
case 'likes':
|
||||
$likes = readData('likes.json', new stdClass());
|
||||
$userLikes = $_SESSION['user_likes'] ?? [];
|
||||
$likes = readData('likes.json', new stdClass());
|
||||
$user = getLoggedInUser();
|
||||
if ($user) {
|
||||
$userLikes = $user['likes'] ?? [];
|
||||
} else {
|
||||
$userLikes = $_SESSION['user_likes'] ?? [];
|
||||
}
|
||||
ok(['likes' => $likes, 'userLikes' => $userLikes]);
|
||||
|
||||
case 'toggle_like':
|
||||
$postId = $body['postId'] ?? '';
|
||||
if (!$postId) err('Missing postId');
|
||||
$likes = readData('likes.json', []);
|
||||
$userLikes = $_SESSION['user_likes'] ?? [];
|
||||
$idx = array_search($postId, $userLikes, true);
|
||||
if ($idx === false) {
|
||||
$likes[$postId] = ($likes[$postId] ?? 0) + 1;
|
||||
$userLikes[] = $postId;
|
||||
$liked = true;
|
||||
$likes = readData('likes.json', []);
|
||||
$user = getLoggedInUser();
|
||||
|
||||
if ($user) {
|
||||
$users = readData('users.json', []);
|
||||
$userLikes = $user['likes'] ?? [];
|
||||
$idx = array_search($postId, $userLikes, true);
|
||||
if ($idx === false) {
|
||||
$likes[$postId] = ($likes[$postId] ?? 0) + 1;
|
||||
$userLikes[] = $postId;
|
||||
$liked = true;
|
||||
} else {
|
||||
$likes[$postId] = max(0, ($likes[$postId] ?? 1) - 1);
|
||||
array_splice($userLikes, $idx, 1);
|
||||
$liked = false;
|
||||
}
|
||||
foreach ($users as &$u) {
|
||||
if ($u['id'] === $user['id']) { $u['likes'] = array_values($userLikes); break; }
|
||||
}
|
||||
unset($u);
|
||||
writeData('users.json', $users);
|
||||
$_SESSION['user_likes'] = array_values($userLikes);
|
||||
} else {
|
||||
$likes[$postId] = max(0, ($likes[$postId] ?? 1) - 1);
|
||||
array_splice($userLikes, $idx, 1);
|
||||
$liked = false;
|
||||
$userLikes = $_SESSION['user_likes'] ?? [];
|
||||
$idx = array_search($postId, $userLikes, true);
|
||||
if ($idx === false) {
|
||||
$likes[$postId] = ($likes[$postId] ?? 0) + 1;
|
||||
$userLikes[] = $postId;
|
||||
$liked = true;
|
||||
} else {
|
||||
$likes[$postId] = max(0, ($likes[$postId] ?? 1) - 1);
|
||||
array_splice($userLikes, $idx, 1);
|
||||
$liked = false;
|
||||
}
|
||||
$_SESSION['user_likes'] = array_values($userLikes);
|
||||
}
|
||||
$_SESSION['user_likes'] = array_values($userLikes);
|
||||
|
||||
writeData('likes.json', $likes);
|
||||
ok(['liked' => $liked, 'count' => $likes[$postId] ?? 0]);
|
||||
|
||||
@@ -801,6 +843,72 @@ switch ($action) {
|
||||
case 'admin_check':
|
||||
ok(['loggedIn' => isAdmin()]);
|
||||
|
||||
// ─── Käyttäjätunnukset ─────────────────────────────────────
|
||||
case 'user_register':
|
||||
$nickname = trim($body['nickname'] ?? '');
|
||||
$email = trim($body['email'] ?? '');
|
||||
$password = $body['password'] ?? '';
|
||||
|
||||
if (!$nickname || !$password) err('Nimimerkki ja salasana vaaditaan.');
|
||||
if (mb_strlen($nickname) < 2 || mb_strlen($nickname) > 30) err('Nimimerkin tulee olla 2–30 merkkiä.');
|
||||
if (mb_strlen($password) < 6) err('Salasanan tulee olla vähintään 6 merkkiä.');
|
||||
if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) err('Sähköpostiosoite ei kelpaa.');
|
||||
|
||||
$users = readData('users.json', []);
|
||||
foreach ($users as $u) {
|
||||
if (mb_strtolower($u['nickname']) === mb_strtolower($nickname)) {
|
||||
err('Nimimerkki on jo käytössä.');
|
||||
}
|
||||
}
|
||||
|
||||
$user = [
|
||||
'id' => 'user_' . time() . '_' . random_int(1000, 9999),
|
||||
'nickname' => htmlspecialchars($nickname, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
||||
'email' => htmlspecialchars($email, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
||||
'password' => password_hash($password, PASSWORD_DEFAULT),
|
||||
'likes' => $_SESSION['user_likes'] ?? [],
|
||||
'created' => date('Y-m-d'),
|
||||
];
|
||||
|
||||
$users[] = $user;
|
||||
writeData('users.json', $users);
|
||||
$_SESSION['tykkaafi_user_id'] = $user['id'];
|
||||
|
||||
ok(['user' => ['id' => $user['id'], 'nickname' => $user['nickname'], 'likes' => $user['likes']]]);
|
||||
|
||||
case 'user_login':
|
||||
$nickname = trim($body['nickname'] ?? '');
|
||||
$password = $body['password'] ?? '';
|
||||
if (!$nickname || !$password) err('Nimimerkki ja salasana vaaditaan.');
|
||||
|
||||
$users = readData('users.json', []);
|
||||
foreach ($users as $u) {
|
||||
if (mb_strtolower($u['nickname']) === mb_strtolower($nickname)) {
|
||||
if (password_verify($password, $u['password'])) {
|
||||
$_SESSION['tykkaafi_user_id'] = $u['id'];
|
||||
$_SESSION['user_likes'] = $u['likes'] ?? [];
|
||||
ok(['user' => ['id' => $u['id'], 'nickname' => $u['nickname'], 'likes' => $u['likes'] ?? []]]);
|
||||
}
|
||||
err('Väärä salasana.');
|
||||
}
|
||||
}
|
||||
err('Nimimerkkiä ei löydy.');
|
||||
|
||||
case 'user_logout':
|
||||
unset($_SESSION['tykkaafi_user_id']);
|
||||
ok();
|
||||
|
||||
case 'user_check':
|
||||
$user = getLoggedInUser();
|
||||
if ($user) {
|
||||
ok(['loggedIn' => true, 'user' => [
|
||||
'id' => $user['id'],
|
||||
'nickname' => $user['nickname'],
|
||||
'likes' => $user['likes'] ?? [],
|
||||
]]);
|
||||
}
|
||||
ok(['loggedIn' => false]);
|
||||
|
||||
default:
|
||||
err('Unknown action');
|
||||
}
|
||||
|
||||
21
index.html
21
index.html
@@ -17,6 +17,7 @@
|
||||
<h1 class="pixel-logo"><span class="logo-heart">♥</span>tykkää<span class="logo-fi">.fi</span></h1>
|
||||
</div>
|
||||
<p class="tagline" data-i18n="tagline"></p>
|
||||
<div class="user-area" id="userArea"></div>
|
||||
<nav>
|
||||
<a href="#posts" data-i18n="nav_posts"></a>
|
||||
<a href="#about" data-i18n="nav_about"></a>
|
||||
@@ -29,7 +30,10 @@
|
||||
<!-- SEARCH & FILTER -->
|
||||
<section class="controls" id="posts">
|
||||
<div class="container">
|
||||
<input type="text" id="search" data-i18n-ph="search_ph" oninput="filterPosts()" />
|
||||
<div class="search-row">
|
||||
<input type="text" id="search" data-i18n-ph="search_ph" oninput="filterPosts()" />
|
||||
<button class="likes-filter-btn" id="likesFilterBtn" onclick="toggleLikesFilter()">❤️ Tykkäämäni</button>
|
||||
</div>
|
||||
<div class="filters" id="categoryFilters"></div>
|
||||
<div class="filters sub-filters" id="subCategoryFilters"></div>
|
||||
<button class="add-post-btn" onclick="openSubmitModal()">✏️ Lisää julkaisu</button>
|
||||
@@ -60,12 +64,7 @@
|
||||
<div class="container">
|
||||
<h2 data-i18n="contact_title"></h2>
|
||||
<p data-i18n="contact_desc"></p>
|
||||
<form class="contact-form" onsubmit="handleSubmit(event)">
|
||||
<input type="text" data-i18n-ph="name_ph" required />
|
||||
<input type="email" data-i18n-ph="email_ph" required />
|
||||
<textarea data-i18n-ph="msg_ph" rows="4" required></textarea>
|
||||
<button type="submit" class="btn" data-i18n="send_btn"></button>
|
||||
</form>
|
||||
<a href="mailto:info@tykkaa.fi" class="contact-email">info@tykkaa.fi</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -190,6 +189,14 @@
|
||||
<img id="lightboxImg" src="" alt="" onclick="event.stopPropagation()" />
|
||||
</div>
|
||||
|
||||
<!-- AUTH MODAL -->
|
||||
<div class="modal-overlay" id="authOverlay" onclick="closeAuthModal()">
|
||||
<div class="modal" onclick="event.stopPropagation()" style="max-width:420px">
|
||||
<button class="modal-close" onclick="closeAuthModal()">✕</button>
|
||||
<div id="authContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
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();
|
||||
|
||||
147
style.css
147
style.css
@@ -324,30 +324,18 @@ nav a:hover { color: #fff; }
|
||||
.contact h2 { font-size: 1.9rem; color: var(--warm-brown); margin-bottom: 8px; }
|
||||
.contact > .container > p { color: var(--text-light); margin-bottom: 28px; }
|
||||
|
||||
.contact-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
max-width: 520px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.contact-form input,
|
||||
.contact-form textarea {
|
||||
padding: 12px 18px;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
.contact-email {
|
||||
font-size: 1.3rem;
|
||||
font-family: 'Georgia', serif;
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
resize: vertical;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.contact-form input:focus,
|
||||
.contact-form textarea:focus { border-color: var(--accent); }
|
||||
.contact-email:hover {
|
||||
color: #a0522d;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* =====================
|
||||
FOOTER
|
||||
@@ -770,6 +758,120 @@ footer {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* =====================
|
||||
USER AREA (header)
|
||||
===================== */
|
||||
.user-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin: 8px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.user-greeting {
|
||||
color: #e8c9a8;
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.user-greeting strong { color: #fff; }
|
||||
|
||||
.user-btn {
|
||||
padding: 6px 16px;
|
||||
border: 2px solid rgba(255,255,255,0.4);
|
||||
border-radius: 20px;
|
||||
background: transparent;
|
||||
color: #f5dfc0;
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-size: 0.82rem;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.user-btn:hover {
|
||||
background: rgba(255,255,255,0.15);
|
||||
color: #fff;
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.register-btn {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.register-btn:hover {
|
||||
background: var(--accent-dark);
|
||||
border-color: var(--accent-dark);
|
||||
}
|
||||
|
||||
/* =====================
|
||||
SEARCH ROW
|
||||
===================== */
|
||||
.search-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-row #search { flex: 1; }
|
||||
|
||||
.likes-filter-btn {
|
||||
padding: 8px 18px;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 20px;
|
||||
background: #fff;
|
||||
color: #c06080;
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.likes-filter-btn:hover {
|
||||
background: #fde8f0;
|
||||
border-color: #e07090;
|
||||
}
|
||||
|
||||
.likes-filter-btn.active {
|
||||
background: #fde8f0;
|
||||
border-color: #e07090;
|
||||
color: #c03060;
|
||||
}
|
||||
|
||||
/* =====================
|
||||
AUTH MODAL
|
||||
===================== */
|
||||
.auth-error {
|
||||
color: #c04040;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 8px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.auth-switch {
|
||||
text-align: center;
|
||||
margin-top: 14px;
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-size: 0.85rem;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.auth-switch:hover {
|
||||
color: var(--accent-dark);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* =====================
|
||||
RESPONSIVE
|
||||
===================== */
|
||||
@@ -779,4 +881,7 @@ footer {
|
||||
.recipe-grid { grid-template-columns: 1fr; }
|
||||
nav { gap: 16px; }
|
||||
.form-row { grid-template-columns: 1fr; }
|
||||
.search-row { flex-direction: column; }
|
||||
.likes-filter-btn { width: 100%; }
|
||||
.user-area { font-size: 0.78rem; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user