- Julkaisualusta resepteille, neuloville, vinkeille - PHP-backend (api.php) palvelinpuolen datalle - Admin-paneeli salasanasuojauksella - Kuvaupload (upload.php) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
242 lines
11 KiB
PHP
242 lines
11 KiB
PHP
<?php
|
||
/**
|
||
* tykkää.fi — API backend
|
||
* Tallentaa julkaisut, kommentit, tykkäykset ja kategoriat JSON-tiedostoihin.
|
||
*/
|
||
session_start();
|
||
header('Content-Type: application/json; charset=UTF-8');
|
||
|
||
// ─── VAIHDA TÄMÄ SALASANA! ──────────────────────────────────────
|
||
define('ADMIN_PASSWORD', 'vaihda_tämä_salasana');
|
||
// ────────────────────────────────────────────────────────────────
|
||
|
||
define('DATA_DIR', __DIR__ . '/data/');
|
||
|
||
if (!is_dir(DATA_DIR)) mkdir(DATA_DIR, 0755, true);
|
||
|
||
$action = $_GET['action'] ?? '';
|
||
$body = [];
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$raw = file_get_contents('php://input');
|
||
$body = json_decode($raw, true) ?? [];
|
||
if (!$action) $action = $body['action'] ?? '';
|
||
}
|
||
|
||
// ─── Apufunktiot ───────────────────────────────────────────────
|
||
function readData(string $file, $default = []) {
|
||
$path = DATA_DIR . $file;
|
||
if (!file_exists($path)) return $default;
|
||
$d = json_decode(file_get_contents($path), true);
|
||
return $d !== null ? $d : $default;
|
||
}
|
||
|
||
function writeData(string $file, $data): void {
|
||
file_put_contents(
|
||
DATA_DIR . $file,
|
||
json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
|
||
);
|
||
}
|
||
|
||
function ok($data = []): void {
|
||
echo json_encode(array_merge(['ok' => true], $data));
|
||
exit;
|
||
}
|
||
|
||
function err(string $msg, int $code = 400): void {
|
||
http_response_code($code);
|
||
echo json_encode(['error' => $msg]);
|
||
exit;
|
||
}
|
||
|
||
function isAdmin(): bool {
|
||
return !empty($_SESSION['tykkaafi_admin']);
|
||
}
|
||
|
||
// ─── Oletusdata ────────────────────────────────────────────────
|
||
function defaultCategories(): array {
|
||
return [
|
||
['id' => 'recipes', 'fi' => 'Reseptit', 'en' => 'Recipes', 'emoji' => '🍳'],
|
||
['id' => 'knitting', 'fi' => 'Neulominen', 'en' => 'Knitting', 'emoji' => '🧶'],
|
||
['id' => 'tips', 'fi' => 'Vinkit', 'en' => 'Tips', 'emoji' => '💡'],
|
||
];
|
||
}
|
||
|
||
function defaultPosts(): array {
|
||
return [
|
||
[
|
||
'id' => 'pancakes', 'emoji' => '🥞', 'title' => 'Kuohkeat letut',
|
||
'category' => 'recipes', 'author' => 'Admin',
|
||
'time' => '20 min', 'servings' => '4 annosta', 'type' => 'recipe',
|
||
'desc' => 'Kultaisia, voisia lettuja, jotka sulavat suuhun. Täydellisiä laiskaan sunnuntaiaamuun.',
|
||
'ingredients' => ['1½ dl vehnäjauhoja','3½ tl leivinjauhetta','1 tl suolaa','1 rkl sokeria','3 dl maitoa','1 muna','3 rkl sulatettua voita','Voita tai öljyä paistamiseen'],
|
||
'steps' => ['Sekoita kulhossa jauhot, leivinjauhe, suola ja sokeri.','Tee keskelle kuoppa ja kaada sekaan maito, muna ja sulatettu voi.','Sekoita tasaiseksi — pienet paakut ovat ok.','Kuumenna pannua keskilämmöllä.','Kaada noin ¼ dl taikinaa per lettu.','Paista kunnes pintaan nousee kuplia, käännä ja paista kullanruskeaksi.','Tarjoile vaahterasiirapilla ja marjoilla.'],
|
||
],
|
||
[
|
||
'id' => 'bolognese', 'emoji' => '🍝', 'title' => 'Klassinen spagetti bolognese',
|
||
'category' => 'recipes', 'author' => 'Admin',
|
||
'time' => '1 t 20 min', 'servings' => '6 annosta', 'type' => 'recipe',
|
||
'desc' => 'Runsas, hitaasti haudutettu lihamauste al dente -spagetin päällä. Ajaton italialainen klassikko.',
|
||
'ingredients' => ['500 g jauhelihaa','400 g spagettia','1 sipuli, hienonnettuna','3 valkosipulinkynttä','800 g murskattuja tomaatteja','2 dl punaviiniä','2 rkl tomaattipyrettä','Suolaa, pippuria, basilikaa, oreganoa','Parmesaanijuustoa tarjoiluun'],
|
||
'steps' => ['Kuullota sipuli ja valkosipuli oliiviöljyssä.','Ruskista jauheliha.','Lisää viini ja anna pelkistyä (5 min).','Lisää tomaatit ja mausteet.','Hauduta miedolla lämmöllä 1 tunti.','Keitä spagetti al denteksi.','Tarjoile parmesaanin kera.'],
|
||
],
|
||
[
|
||
'id' => 'cookies', 'emoji' => '🍪', 'title' => 'Suklaahippukeksit',
|
||
'category' => 'recipes', 'author' => 'Admin',
|
||
'time' => '30 min', 'servings' => '24 keksiä', 'type' => 'recipe',
|
||
'desc' => 'Sitkeä sisältä, rapea reunoilta — täydellinen kotitekoinen keksi.',
|
||
'ingredients' => ['5½ dl vehnäjauhoja','1 tl ruokasoodaa','1 tl suolaa','225 g pehmeää voita','1½ dl sokeria','1½ dl ruskeaa sokeria','2 munaa','2 tl vaniljauutetta','4 dl suklaahippuja'],
|
||
'steps' => ['Kuumenna uuni 190°C.','Sekoita jauhot, sooda ja suola.','Vatkaa voi ja sokerit kuohkeaksi.','Lisää munat ja vanilja.','Yhdistä aineet ja lisää suklaa.','Lusikoi pellille.','Paista 9–11 min kullanruskeaksi.'],
|
||
],
|
||
[
|
||
'id' => 'soup', 'emoji' => '🍲', 'title' => 'Täyttävä kasviskeitto',
|
||
'category' => 'recipes', 'author' => 'Admin',
|
||
'time' => '45 min', 'servings' => '4 annosta', 'type' => 'recipe',
|
||
'desc' => 'Lämmittävä kulhollinen paksuja kasviksia rikkaassa yrttiliemessä.',
|
||
'ingredients' => ['2 rkl oliiviöljyä','1 sipuli','3 valkosipulia','3 porkkanaa, siivuina','2 perunaa, kuutioina','1 kesäkurpitsa','400 g tomaattimurskaa','1½ l kasvislientä','Timjami, rosmariini, suola, pippuri'],
|
||
'steps' => ['Kuullota sipuli ja valkosipuli.','Lisää kasvikset ja sekoittele 5 min.','Kaada liemi ja tomaatit, lisää yrtit.','Hauduta 25 min.','Lisää kesäkurpitsa, keitä 10 min.','Mausta ja tarjoile.'],
|
||
],
|
||
[
|
||
'id' => 'knitting_scarf', 'emoji' => '🧶', 'title' => 'Helppo huivi aloittelijalle',
|
||
'category' => 'knitting', 'author' => 'Admin', 'type' => 'post',
|
||
'desc' => 'Neulo kaunis huivi muutamassa tunnissa — täydellinen ensimmäinen projekti!',
|
||
'body' => '<p><strong>Tarvitset:</strong></p><ul><li>Paksu lanka (noin 200 g)</li><li>Pyöröneulat nro 6–8</li></ul><p><strong>Ohje:</strong> Luo 20 silmukkaa. Neulo suoraan (edestakaisin oikein silmukoin) kunnes huivi on noin 150 cm pitkä. Päättele silmukat ja viimeistele päät. Valmis! 🎉</p><p>Vinkki: Paksuilla langoilla ja suurilla neuloilla saat huivista nopeasti valmiin, vaikka olisit aloittelija.</p>',
|
||
],
|
||
[
|
||
'id' => 'tip_morning', 'emoji' => '💡', 'title' => 'Rauhallinen aamurutiini',
|
||
'category' => 'tips', 'author' => 'Admin', 'type' => 'post',
|
||
'desc' => 'Pienet muutokset aamurutiiniin tekevät koko päivästä paremman.',
|
||
'body' => '<p>Kokeile näitä vinkkejä parempaan aamuun:</p><ul><li>🌅 Herää 15 minuuttia aiemmin kuin tarvitset</li><li>💧 Juo lasi vettä ennen kahvia</li><li>📵 Älä katso puhelinta heti herätessä</li><li>🚶 Pieni lenkki tai venyttely ennen päivää</li></ul><p>Pienet muutokset, iso vaikutus!</p>',
|
||
],
|
||
];
|
||
}
|
||
|
||
function getOrInitPosts(): array {
|
||
if (!file_exists(DATA_DIR . 'posts.json')) {
|
||
$posts = defaultPosts();
|
||
writeData('posts.json', $posts);
|
||
return $posts;
|
||
}
|
||
return readData('posts.json', []);
|
||
}
|
||
|
||
function getOrInitCategories(): array {
|
||
if (!file_exists(DATA_DIR . 'categories.json')) {
|
||
$cats = defaultCategories();
|
||
writeData('categories.json', $cats);
|
||
return $cats;
|
||
}
|
||
return readData('categories.json', []);
|
||
}
|
||
|
||
// ─── Routing ───────────────────────────────────────────────────
|
||
switch ($action) {
|
||
|
||
case 'posts':
|
||
ok(['posts' => getOrInitPosts()]);
|
||
|
||
case 'categories':
|
||
ok(['categories' => getOrInitCategories()]);
|
||
|
||
case 'likes':
|
||
$likes = readData('likes.json', new stdClass());
|
||
$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;
|
||
} else {
|
||
$likes[$postId] = max(0, ($likes[$postId] ?? 1) - 1);
|
||
array_splice($userLikes, $idx, 1);
|
||
$liked = false;
|
||
}
|
||
$_SESSION['user_likes'] = array_values($userLikes);
|
||
writeData('likes.json', $likes);
|
||
ok(['liked' => $liked, 'count' => $likes[$postId] ?? 0]);
|
||
|
||
case 'comments':
|
||
$postId = $_GET['postId'] ?? '';
|
||
$comments = readData('comments.json', []);
|
||
ok(['comments' => $comments[$postId] ?? []]);
|
||
|
||
case 'add_comment':
|
||
$postId = $body['postId'] ?? '';
|
||
$name = trim($body['name'] ?? '');
|
||
$text = trim($body['text'] ?? '');
|
||
if (!$postId || !$name || !$text) err('Missing fields');
|
||
$now = time();
|
||
$win = 10 * 60;
|
||
$times = array_values(array_filter($_SESSION['comment_times'] ?? [], fn($t) => $now - $t < $win));
|
||
if (count($times) >= 3) err('Liian monta kommenttia. Odota hetki.');
|
||
$times[] = $now;
|
||
$_SESSION['comment_times'] = $times;
|
||
$comments = readData('comments.json', []);
|
||
if (!isset($comments[$postId])) $comments[$postId] = [];
|
||
$comment = [
|
||
'name' => htmlspecialchars($name, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
||
'text' => htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
||
'time' => date('d.m.Y'),
|
||
];
|
||
$comments[$postId][] = $comment;
|
||
writeData('comments.json', $comments);
|
||
ok(['comment' => $comment]);
|
||
|
||
case 'add_post':
|
||
$post = $body['post'] ?? [];
|
||
if (empty($post['title'])) err('Missing title');
|
||
$post['id'] = preg_replace('/[^a-z0-9_]/', '', strtolower($post['id'] ?? '')) ?: 'post_' . time();
|
||
$posts = getOrInitPosts();
|
||
$posts[] = $post;
|
||
writeData('posts.json', $posts);
|
||
ok();
|
||
|
||
case 'update_post':
|
||
if (!isAdmin()) err('Unauthorized', 403);
|
||
$post = $body['post'] ?? [];
|
||
if (empty($post['id'])) err('Missing id');
|
||
$posts = getOrInitPosts();
|
||
foreach ($posts as &$p) {
|
||
if ($p['id'] === $post['id']) { $p = $post; break; }
|
||
}
|
||
unset($p);
|
||
writeData('posts.json', $posts);
|
||
ok();
|
||
|
||
case 'delete_post':
|
||
if (!isAdmin()) err('Unauthorized', 403);
|
||
$id = $body['id'] ?? '';
|
||
if (!$id) err('Missing id');
|
||
$posts = array_values(array_filter(getOrInitPosts(), fn($p) => ($p['id'] ?? '') !== $id));
|
||
writeData('posts.json', $posts);
|
||
ok();
|
||
|
||
case 'save_categories':
|
||
if (!isAdmin()) err('Unauthorized', 403);
|
||
writeData('categories.json', $body['categories'] ?? []);
|
||
ok();
|
||
|
||
case 'admin_login':
|
||
if (($body['password'] ?? '') === ADMIN_PASSWORD) {
|
||
$_SESSION['tykkaafi_admin'] = true;
|
||
ok();
|
||
}
|
||
err('Väärä salasana');
|
||
|
||
case 'admin_logout':
|
||
$_SESSION['tykkaafi_admin'] = false;
|
||
ok();
|
||
|
||
case 'admin_check':
|
||
ok(['loggedIn' => isAdmin()]);
|
||
|
||
default:
|
||
err('Unknown action');
|
||
}
|