Alikategoria-parannukset: monivalinta, Muut-kategoria, emoji pois administa
- api.php: lisätään 'Muut'-alikategoria Resepteihin, Neulomiseen ja Vinkkeihin - api.php: getOrInitCategories() lisää puuttuvat alikategoriat automaattisesti - admin.html: poistetaan emoji-kenttä (emoji tulee kategoriasta automaattisesti) - admin.html: alikategoriat checkboxeina → voi valita useamman samanaikaisesti - script.js: subcategory tallennetaan aina taulukkona - script.js: filterPosts tukee pilkulla eroteltuja alikategorioita (postSubs.includes) - script.js: renderCards kirjoittaa subcategory-arrayn pilkulla eroteltuna data-attribuuttiin Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
57
admin.html
57
admin.html
@@ -245,15 +245,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-group">
|
||||||
<div class="form-group">
|
<label id="lbl_category">Kategoria</label>
|
||||||
<label id="lbl_category">Kategoria</label>
|
<select id="postCategory" onchange="updateAdminSubcategories()"></select>
|
||||||
<select id="postCategory"></select>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group" id="adminSubcatGroup" style="display:none">
|
||||||
<label id="lbl_emoji">Emoji</label>
|
<label>Alikategoriat <small style="color:#7a5c3e;font-weight:normal;text-transform:none">(voit valita useamman)</small></label>
|
||||||
<input type="text" id="postEmoji" placeholder="🍽️" maxlength="4" />
|
<div id="adminSubcatCheckboxes" style="display:flex;flex-wrap:wrap;gap:8px;margin-top:4px"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -600,6 +599,26 @@
|
|||||||
const current = sel.value;
|
const current = sel.value;
|
||||||
sel.innerHTML = ADMIN.categories.map(c => `<option value="${c.id}">${c.emoji || ''} ${c.fi} / ${c.en}</option>`).join('');
|
sel.innerHTML = ADMIN.categories.map(c => `<option value="${c.id}">${c.emoji || ''} ${c.fi} / ${c.en}</option>`).join('');
|
||||||
if (current) sel.value = current;
|
if (current) sel.value = current;
|
||||||
|
updateAdminSubcategories();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAdminSubcategories(checked = []) {
|
||||||
|
const catId = document.getElementById('postCategory').value;
|
||||||
|
const cat = ADMIN.categories.find(c => c.id === catId);
|
||||||
|
const subs = cat?.subcategories || [];
|
||||||
|
const group = document.getElementById('adminSubcatGroup');
|
||||||
|
const box = document.getElementById('adminSubcatCheckboxes');
|
||||||
|
if (!subs.length) { group.style.display = 'none'; box.innerHTML = ''; return; }
|
||||||
|
group.style.display = '';
|
||||||
|
box.innerHTML = subs.map(s => `
|
||||||
|
<label style="display:flex;align-items:center;gap:5px;cursor:pointer;background:#f5f0eb;padding:5px 10px;border-radius:6px;font-size:0.88rem">
|
||||||
|
<input type="checkbox" value="${s.id}" ${checked.includes(s.id) ? 'checked' : ''} style="accent-color:#8b5e3c">
|
||||||
|
${s.fi}
|
||||||
|
</label>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCheckedSubcategories() {
|
||||||
|
return [...document.querySelectorAll('#adminSubcatCheckboxes input[type=checkbox]:checked')].map(cb => cb.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================
|
// ===========================
|
||||||
@@ -645,11 +664,13 @@
|
|||||||
let editingId = null;
|
let editingId = null;
|
||||||
|
|
||||||
async function savePost() {
|
async function savePost() {
|
||||||
const title = document.getElementById('postTitle').value.trim();
|
const title = document.getElementById('postTitle').value.trim();
|
||||||
const emoji = document.getElementById('postEmoji').value.trim() || '🍽️';
|
const category = document.getElementById('postCategory').value;
|
||||||
const category = document.getElementById('postCategory').value;
|
const subcategory = getCheckedSubcategories();
|
||||||
const author = document.getElementById('postAuthor').value.trim();
|
const cat = ADMIN.categories.find(c => c.id === category);
|
||||||
const desc = document.getElementById('postDesc').value.trim();
|
const emoji = cat?.emoji || '📝';
|
||||||
|
const author = document.getElementById('postAuthor').value.trim();
|
||||||
|
const desc = document.getElementById('postDesc').value.trim();
|
||||||
|
|
||||||
if (!title) { showToast(at('no_title')); return; }
|
if (!title) { showToast(at('no_title')); return; }
|
||||||
|
|
||||||
@@ -658,7 +679,7 @@
|
|||||||
document.getElementById('postImg2').value.trim(),
|
document.getElementById('postImg2').value.trim(),
|
||||||
document.getElementById('postImg3').value.trim(),
|
document.getElementById('postImg3').value.trim(),
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
const post = { title, emoji, category, author, desc, images, type: currentType };
|
const post = { title, emoji, category, subcategory, author, desc, images, type: currentType };
|
||||||
|
|
||||||
if (currentType === 'recipe') {
|
if (currentType === 'recipe') {
|
||||||
post.time = document.getElementById('postTime').value.trim();
|
post.time = document.getElementById('postTime').value.trim();
|
||||||
@@ -698,9 +719,11 @@
|
|||||||
editingId = id;
|
editingId = id;
|
||||||
document.getElementById('formTitle').textContent = at('form_edit');
|
document.getElementById('formTitle').textContent = at('form_edit');
|
||||||
document.getElementById('postTitle').value = p.title;
|
document.getElementById('postTitle').value = p.title;
|
||||||
document.getElementById('postEmoji').value = p.emoji;
|
|
||||||
document.getElementById('postCategory').value = p.category;
|
document.getElementById('postCategory').value = p.category;
|
||||||
document.getElementById('postAuthor').value = p.author || '';
|
document.getElementById('postAuthor').value = p.author || '';
|
||||||
|
// Load subcategory checkboxes with existing values
|
||||||
|
const existingSubs = Array.isArray(p.subcategory) ? p.subcategory : (p.subcategory ? [p.subcategory] : []);
|
||||||
|
updateAdminSubcategories(existingSubs);
|
||||||
document.getElementById('postDesc').value = p.desc || '';
|
document.getElementById('postDesc').value = p.desc || '';
|
||||||
setAdmImgPreview(1, p.images?.[0] || '');
|
setAdmImgPreview(1, p.images?.[0] || '');
|
||||||
setAdmImgPreview(2, p.images?.[1] || '');
|
setAdmImgPreview(2, p.images?.[1] || '');
|
||||||
@@ -735,8 +758,8 @@
|
|||||||
function resetForm() {
|
function resetForm() {
|
||||||
editingId = null;
|
editingId = null;
|
||||||
document.getElementById('postTitle').value = '';
|
document.getElementById('postTitle').value = '';
|
||||||
document.getElementById('postEmoji').value = '';
|
|
||||||
document.getElementById('postAuthor').value = '';
|
document.getElementById('postAuthor').value = '';
|
||||||
|
updateAdminSubcategories([]);
|
||||||
document.getElementById('postDesc').value = '';
|
document.getElementById('postDesc').value = '';
|
||||||
document.getElementById('postTime').value = '';
|
document.getElementById('postTime').value = '';
|
||||||
document.getElementById('postServings').value = '';
|
document.getElementById('postServings').value = '';
|
||||||
|
|||||||
22
api.php
22
api.php
@@ -60,15 +60,19 @@ function defaultCategories(): array {
|
|||||||
['id' => 'kasvis', 'fi' => 'Kasvis'],
|
['id' => 'kasvis', 'fi' => 'Kasvis'],
|
||||||
['id' => 'vegaaniset', 'fi' => 'Vegaaniset'],
|
['id' => 'vegaaniset', 'fi' => 'Vegaaniset'],
|
||||||
['id' => 'jalkiruuat', 'fi' => 'Jälkiruuat'],
|
['id' => 'jalkiruuat', 'fi' => 'Jälkiruuat'],
|
||||||
|
['id' => 'muut', 'fi' => 'Muut'],
|
||||||
]],
|
]],
|
||||||
['id' => 'knitting', 'fi' => 'Neulominen', 'en' => 'Knitting', 'emoji' => '🧶',
|
['id' => 'knitting', 'fi' => 'Neulominen', 'en' => 'Knitting', 'emoji' => '🧶',
|
||||||
'subcategories' => [
|
'subcategories' => [
|
||||||
['id' => 'aloittelijoille', 'fi' => 'Aloittelijoille'],
|
['id' => 'aloittelijoille', 'fi' => 'Aloittelijoille'],
|
||||||
['id' => 'vaatteet', 'fi' => 'Vaatteet'],
|
['id' => 'vaatteet', 'fi' => 'Vaatteet'],
|
||||||
['id' => 'kodin_tekstiilit','fi' => 'Kodin tekstiilit'],
|
['id' => 'kodin_tekstiilit','fi' => 'Kodin tekstiilit'],
|
||||||
|
['id' => 'muut', 'fi' => 'Muut'],
|
||||||
]],
|
]],
|
||||||
['id' => 'tips', 'fi' => 'Vinkit', 'en' => 'Tips', 'emoji' => '💡',
|
['id' => 'tips', 'fi' => 'Vinkit', 'en' => 'Tips', 'emoji' => '💡',
|
||||||
'subcategories' => []],
|
'subcategories' => [
|
||||||
|
['id' => 'muut', 'fi' => 'Muut'],
|
||||||
|
]],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,15 +320,25 @@ function getOrInitCategories(): array {
|
|||||||
return $cats;
|
return $cats;
|
||||||
}
|
}
|
||||||
$cats = readData('categories.json', []);
|
$cats = readData('categories.json', []);
|
||||||
// Merge in subcategories if existing file doesn't have them yet
|
// Merge in subcategories (and new ones) from defaults if missing
|
||||||
$defaults = defaultCategories();
|
$defaults = defaultCategories();
|
||||||
$defaultMap = [];
|
$defaultMap = [];
|
||||||
foreach ($defaults as $d) { $defaultMap[$d['id']] = $d; }
|
foreach ($defaults as $d) { $defaultMap[$d['id']] = $d; }
|
||||||
$changed = false;
|
$changed = false;
|
||||||
foreach ($cats as &$cat) {
|
foreach ($cats as &$cat) {
|
||||||
if (!isset($cat['subcategories']) && isset($defaultMap[$cat['id']]['subcategories'])) {
|
$defSubs = $defaultMap[$cat['id']]['subcategories'] ?? [];
|
||||||
$cat['subcategories'] = $defaultMap[$cat['id']]['subcategories'];
|
if (!isset($cat['subcategories'])) {
|
||||||
|
$cat['subcategories'] = $defSubs;
|
||||||
$changed = true;
|
$changed = true;
|
||||||
|
} else {
|
||||||
|
// Add any new subcategory ids from defaults that don't exist yet
|
||||||
|
$existingIds = array_column($cat['subcategories'], 'id');
|
||||||
|
foreach ($defSubs as $ds) {
|
||||||
|
if (!in_array($ds['id'], $existingIds)) {
|
||||||
|
$cat['subcategories'][] = $ds;
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unset($cat);
|
unset($cat);
|
||||||
|
|||||||
10
script.js
10
script.js
@@ -177,7 +177,7 @@ function renderCards() {
|
|||||||
? `<div class="card-img card-has-photo"><img src="${imgSrc}" alt="${p.title}" /></div>`
|
? `<div class="card-img card-has-photo"><img src="${imgSrc}" alt="${p.title}" /></div>`
|
||||||
: `<div class="card-img">${p.emoji}</div>`;
|
: `<div class="card-img">${p.emoji}</div>`;
|
||||||
return `
|
return `
|
||||||
<article class="recipe-card" data-category="${p.category}" data-subcategory="${p.subcategory || ''}">
|
<article class="recipe-card" data-category="${p.category}" data-subcategory="${Array.isArray(p.subcategory) ? p.subcategory.join(',') : (p.subcategory || '')}">
|
||||||
${cardImgHTML}
|
${cardImgHTML}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<span class="category-tag">${getCategoryLabel(p.category)}</span>
|
<span class="category-tag">${getCategoryLabel(p.category)}</span>
|
||||||
@@ -237,8 +237,9 @@ function filterPosts() {
|
|||||||
const cards = document.querySelectorAll('.recipe-card');
|
const cards = document.querySelectorAll('.recipe-card');
|
||||||
let visible = 0;
|
let visible = 0;
|
||||||
cards.forEach(card => {
|
cards.forEach(card => {
|
||||||
const matchesCat = currentFilter === 'all' || card.dataset.category === currentFilter;
|
const matchesCat = currentFilter === 'all' || card.dataset.category === currentFilter;
|
||||||
const matchesSub = currentSubFilter === 'all' || card.dataset.subcategory === currentSubFilter;
|
const postSubs = (card.dataset.subcategory || '').split(',').filter(Boolean);
|
||||||
|
const matchesSub = currentSubFilter === 'all' || postSubs.includes(currentSubFilter);
|
||||||
const title = (card.querySelector('h3')?.textContent || '').toLowerCase();
|
const title = (card.querySelector('h3')?.textContent || '').toLowerCase();
|
||||||
const desc = (card.querySelector('p:not(.card-author)')?.textContent || '').toLowerCase();
|
const desc = (card.querySelector('p:not(.card-author)')?.textContent || '').toLowerCase();
|
||||||
const subLbl = (card.dataset.subcategory || '').toLowerCase();
|
const subLbl = (card.dataset.subcategory || '').toLowerCase();
|
||||||
@@ -526,7 +527,8 @@ async function submitPublicPost() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const category = document.getElementById('sub-category').value;
|
const category = document.getElementById('sub-category').value;
|
||||||
const subcategory = document.getElementById('sub-subcategory')?.value || '';
|
const subVal = document.getElementById('sub-subcategory')?.value || '';
|
||||||
|
const subcategory = subVal ? [subVal] : [];
|
||||||
const cat = APP.categories.find(c => c.id === category);
|
const cat = APP.categories.find(c => c.id === category);
|
||||||
const emoji = cat?.emoji || '📝';
|
const emoji = cat?.emoji || '📝';
|
||||||
const author = document.getElementById('sub-author').value.trim() || 'Vieras';
|
const author = document.getElementById('sub-author').value.trim() || 'Vieras';
|
||||||
|
|||||||
Reference in New Issue
Block a user