GameJam-Seite integriert
- public/gamejam.html: teilnehmerseitige AI-GameJam-Seite (TechDay Bruchsal, 27.05.) mit eigenem Schwarz-Weiß-Styling, Zurück-Link zur Landingpage - server.js: abhängigkeitsfreier Multipart-Parser plus Endpunkte POST /api/gamejam/upload und GET /api/gamejam/games - index.html: Navigations-Verweis "GameJam" in Header und Mobilmenü - .gitignore: hochgeladene Spiele und Einreichungen ausgeschlossen Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
143748dc0d
commit
b629bbc90a
4 files changed
+918
-5
No files matched your search
@@ -1,7 +1,10 @@
|
||||
/* ============================================================
|
||||
ToolCTRL — Landing Page Server
|
||||
Abhängigkeitsfrei (nur Node-Standardbibliothek).
|
||||
Liefert /public und nimmt Leads unter POST /api/lead entgegen.
|
||||
- Liefert /public
|
||||
- POST /api/lead — Kontakt-Leads
|
||||
- POST /api/gamejam/upload — GameJam: Spiel einreichen (Datei-Upload)
|
||||
- GET /api/gamejam/games — GameJam: eingereichte Spiele
|
||||
============================================================ */
|
||||
|
||||
"use strict";
|
||||
@@ -9,6 +12,7 @@
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const ROOT = __dirname;
|
||||
const PUBLIC_DIR = path.join(ROOT, "public");
|
||||
@@ -16,6 +20,12 @@ const LEADS_DIR = path.join(ROOT, "leads");
|
||||
const LEADS_JSON = path.join(LEADS_DIR, "leads.ndjson");
|
||||
const LEADS_CSV = path.join(LEADS_DIR, "leads.csv");
|
||||
|
||||
/* GameJam-Speicher */
|
||||
const GAMEJAM_DIR = path.join(ROOT, "gamejam");
|
||||
const SUBMISSIONS_JSON = path.join(GAMEJAM_DIR, "submissions.ndjson");
|
||||
const GAMES_DIR = path.join(PUBLIC_DIR, "gamejam-games");
|
||||
const MAX_UPLOAD = 5 * 1024 * 1024; /* 5 MB pro eingereichtem Spiel */
|
||||
|
||||
/* Bevorzugte Ports — der erste freie wird genutzt. */
|
||||
const PORT_CANDIDATES = [4317, 4318, 4417, 5317, 8317, 0];
|
||||
const HOST = "127.0.0.1";
|
||||
@@ -45,6 +55,12 @@ function ensureLeadStore() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- GameJam-Speicher vorbereiten ---------- */
|
||||
function ensureGamejamStore() {
|
||||
if (!fs.existsSync(GAMEJAM_DIR)) fs.mkdirSync(GAMEJAM_DIR, { recursive: true });
|
||||
if (!fs.existsSync(GAMES_DIR)) fs.mkdirSync(GAMES_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function csvCell(value) {
|
||||
const s = String(value == null ? "" : value);
|
||||
return '"' + s.replace(/"/g, '""').replace(/\r?\n/g, " ") + '"';
|
||||
@@ -162,6 +178,154 @@ function handleLead(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Multipart-Parser (abhängigkeitsfrei) ---------- */
|
||||
function parseMultipart(buffer, boundary) {
|
||||
const result = { fields: {}, files: [] };
|
||||
const delim = Buffer.from("--" + boundary);
|
||||
let start = buffer.indexOf(delim);
|
||||
if (start < 0) return result;
|
||||
start += delim.length;
|
||||
|
||||
while (start < buffer.length) {
|
||||
/* "--" direkt nach dem Delimiter => Ende */
|
||||
if (buffer[start] === 0x2d && buffer[start + 1] === 0x2d) break;
|
||||
if (buffer[start] === 0x0d && buffer[start + 1] === 0x0a) start += 2;
|
||||
|
||||
const next = buffer.indexOf(delim, start);
|
||||
if (next < 0) break;
|
||||
|
||||
let partEnd = next;
|
||||
if (buffer[partEnd - 2] === 0x0d && buffer[partEnd - 1] === 0x0a) partEnd -= 2;
|
||||
const part = buffer.slice(start, partEnd);
|
||||
start = next + delim.length;
|
||||
|
||||
const headerEnd = part.indexOf("\r\n\r\n");
|
||||
if (headerEnd < 0) continue;
|
||||
const headerStr = part.slice(0, headerEnd).toString("utf8");
|
||||
const content = part.slice(headerEnd + 4);
|
||||
|
||||
const nameM = headerStr.match(/name="([^"]*)"/i);
|
||||
if (!nameM) continue;
|
||||
const fileM = headerStr.match(/filename="([^"]*)"/i);
|
||||
|
||||
if (fileM) {
|
||||
result.files.push({
|
||||
field: nameM[1],
|
||||
filename: fileM[1],
|
||||
data: content
|
||||
});
|
||||
} else {
|
||||
result.fields[nameM[1]] = content.toString("utf8");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ---------- GameJam: Spiel einreichen ---------- */
|
||||
function handleGamejamUpload(req, res) {
|
||||
const ctype = req.headers["content-type"] || "";
|
||||
const bM = ctype.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
|
||||
if (!/multipart\/form-data/i.test(ctype) || !bM) {
|
||||
return sendJSON(res, 400, { error: "Ungültiges Upload-Format." });
|
||||
}
|
||||
const boundary = (bM[1] || bM[2]).trim();
|
||||
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
let tooLarge = false;
|
||||
|
||||
req.on("data", (chunk) => {
|
||||
size += chunk.length;
|
||||
if (size > MAX_UPLOAD) {
|
||||
tooLarge = true;
|
||||
req.destroy();
|
||||
return;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
req.on("end", () => {
|
||||
if (tooLarge) {
|
||||
return sendJSON(res, 413, { error: "Datei zu groß (max. 5 MB)." });
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = parseMultipart(Buffer.concat(chunks), boundary);
|
||||
} catch (e) {
|
||||
return sendJSON(res, 400, { error: "Upload konnte nicht gelesen werden." });
|
||||
}
|
||||
|
||||
const teilnehmer = String(parsed.fields.teilnehmer || "").trim();
|
||||
const game = String(parsed.fields.game || "").trim();
|
||||
const file = parsed.files.find((f) => f.field === "file");
|
||||
|
||||
if (!teilnehmer || !game) {
|
||||
return sendJSON(res, 400, { error: "Teilnehmer und Game-Name angeben." });
|
||||
}
|
||||
if (!file || !file.data || file.data.length === 0) {
|
||||
return sendJSON(res, 400, { error: "Keine Datei ausgewählt." });
|
||||
}
|
||||
if (!/\.html?$/i.test(file.filename || "")) {
|
||||
return sendJSON(res, 400, { error: "Nur einzelne .html-Dateien." });
|
||||
}
|
||||
|
||||
const id = Date.now().toString(36) + "-" + crypto.randomBytes(4).toString("hex");
|
||||
const entry = {
|
||||
id: id,
|
||||
zeitpunkt: new Date().toISOString(),
|
||||
teilnehmer: teilnehmer.slice(0, 200),
|
||||
game: game.slice(0, 200),
|
||||
url: "/gamejam-games/" + id + ".html"
|
||||
};
|
||||
|
||||
try {
|
||||
ensureGamejamStore();
|
||||
fs.writeFileSync(path.join(GAMES_DIR, id + ".html"), file.data);
|
||||
fs.appendFileSync(SUBMISSIONS_JSON, JSON.stringify(entry) + "\n", "utf8");
|
||||
} catch (e) {
|
||||
console.error("[gamejam] Speichern fehlgeschlagen:", e.message);
|
||||
return sendJSON(res, 500, { error: "Speichern fehlgeschlagen." });
|
||||
}
|
||||
|
||||
console.log(
|
||||
"[gamejam] " + entry.zeitpunkt + " — \"" + entry.game +
|
||||
"\" von " + entry.teilnehmer
|
||||
);
|
||||
sendJSON(res, 200, { ok: true, id: id });
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- GameJam: eingereichte Spiele ---------- */
|
||||
function handleGamejamGames(req, res) {
|
||||
let games = [];
|
||||
try {
|
||||
if (fs.existsSync(SUBMISSIONS_JSON)) {
|
||||
games = fs
|
||||
.readFileSync(SUBMISSIONS_JSON, "utf8")
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.map((g) => ({
|
||||
id: g.id,
|
||||
teilnehmer: g.teilnehmer,
|
||||
game: g.game,
|
||||
url: g.url
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[gamejam] Galerie konnte nicht gelesen werden:", e.message);
|
||||
}
|
||||
sendJSON(res, 200, games);
|
||||
}
|
||||
|
||||
function sendJSON(res, code, obj) {
|
||||
res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" });
|
||||
res.end(JSON.stringify(obj));
|
||||
@@ -178,6 +342,20 @@ const server = http.createServer((req, res) => {
|
||||
return handleLead(req, res);
|
||||
}
|
||||
|
||||
if (route === "/api/gamejam/upload") {
|
||||
if (req.method !== "POST") {
|
||||
return sendJSON(res, 405, { error: "Methode nicht erlaubt." });
|
||||
}
|
||||
return handleGamejamUpload(req, res);
|
||||
}
|
||||
|
||||
if (route === "/api/gamejam/games") {
|
||||
if (req.method !== "GET") {
|
||||
return sendJSON(res, 405, { error: "Methode nicht erlaubt." });
|
||||
}
|
||||
return handleGamejamGames(req, res);
|
||||
}
|
||||
|
||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||
res.writeHead(405).end("Method Not Allowed");
|
||||
return;
|
||||
@@ -204,13 +382,15 @@ function listen(candidates) {
|
||||
server.listen(port, HOST, () => {
|
||||
const actual = server.address().port;
|
||||
ensureLeadStore();
|
||||
ensureGamejamStore();
|
||||
console.log("");
|
||||
console.log(" ToolCTRL — Landing Page läuft");
|
||||
console.log(" ----------------------------------------");
|
||||
console.log(" URL: http://" + HOST + ":" + actual);
|
||||
console.log(" Leads: " + LEADS_CSV);
|
||||
console.log(" " + LEADS_JSON);
|
||||
console.log(" Stop: Strg + C");
|
||||
console.log(" URL: http://" + HOST + ":" + actual);
|
||||
console.log(" GameJam: http://" + HOST + ":" + actual + "/gamejam.html");
|
||||
console.log(" Leads: " + LEADS_CSV);
|
||||
console.log(" GameJam-Einreichungen: " + SUBMISSIONS_JSON);
|
||||
console.log(" Stop: Strg + C");
|
||||
console.log("");
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user