Initial commit: tykkää.fi sivusto

- 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>
This commit is contained in:
Jukka Lampikoski
2026-03-08 00:20:17 +02:00
commit 4248e69ab7
11 changed files with 2684 additions and 0 deletions

241
api.php Normal file
View File

@@ -0,0 +1,241 @@
<?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 911 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 68</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');
}