chahinebrini a81ba2e54a feat(community): Post.gameName + GameShareBanner-rendering chain
Adds optional `gameName` column to community_posts so game-share posts
can render with the game-banner above the post-content (Snake/Tetris/
Memory/TTT visual indicator).

- prisma/schema.prisma: CommunityPost.gameName String? @map("game_name")
- migration: ALTER TABLE rebreak.community_posts ADD COLUMN game_name
- db/community.ts: createPost() accepts gameName param
- api/community/post.post.ts: extracts gameName from body
- api/community/posts.get.ts: returns gameName, prefers DB over content-parse

Frontend (already in flight on upgrade/sdk-54): PostCard.tsx renders
GameShareBanner when post.category === 'game_share' && post.gameName.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:28:07 +02:00

83 lines
2.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { awardPoints } from "../../utils/scoring";
import { createPost } from "../../db/community";
const MODERATION_PROMPT = `Du moderierst Beiträge in einer anonymen Selbsthilfe-Community für Menschen mit Glücksspielsucht.
Entferne Beiträge die:
- Casino-Werbung oder Links zu Glücksspielseiten enthalten
- Tipps zum Umgehen von Blockern geben
- Andere Nutzer manipulieren oder angreifen
- Offensichtlichen Spam darstellen
Erlaube: persönliche Erfahrungen, Hilferufe, Erfolgsgeschichten, Fragen.
Antworte NUR mit JSON: {"approved": boolean, "reason": string}`;
export default defineEventHandler(async (event) => {
const user = await requireUser(event);
const body = await readBody(event);
const { category, content, imageUrl, gameName } = body as {
category: string;
content: string;
imageUrl?: string;
gameName?: string | null;
};
if (!content?.trim() || !category) {
throw createError({
statusCode: 400,
message: "category und content erforderlich",
});
}
const config = useRuntimeConfig();
// Moderate via OpenRouter (optional skip if no API key)
if (config.openrouterApiKey) {
try {
const modResponse = await $fetch<{
choices: { message: { content: string } }[];
}>("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${config.openrouterApiKey}`,
"Content-Type": "application/json",
"HTTP-Referer": "https://rebreak.org",
"X-Title": "ReBreak Moderation",
},
body: {
model: "meta-llama/llama-3.2-3b-instruct:free",
max_tokens: 100,
messages: [
{ role: "system", content: MODERATION_PROMPT },
{
role: "user",
content: `Kategorie: ${category}\nInhalt: ${content}`,
},
],
},
});
const raw = modResponse.choices?.[0]?.message?.content ?? "";
const jsonMatch = raw.match(/\{[\s\S]*\}/);
const modResult: { approved: boolean; reason: string } = jsonMatch
? JSON.parse(jsonMatch[0])
: { approved: true, reason: "" };
if (!modResult.approved) {
throw createError({
statusCode: 422,
message: `Beitrag abgelehnt: ${modResult.reason}`,
});
}
} catch (err: any) {
if (err.statusCode === 422) throw err;
}
}
const data = await createPost(user.id, category, content.trim(), imageUrl, gameName ?? null);
// Punkte vergeben
await awardPoints(user.id, "post_created", { post_id: data.id });
return data;
});