Dnevni recept: tabela daily_recipes, API /api/recipes, prikaz na /rezepte
This commit is contained in:
parent
001a05fe16
commit
b92c39d9f0
@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
import { ChefHat, Clock, Users, X, ChevronLeft } from "lucide-react";
|
import { ChefHat, Clock, Users, X, ChevronLeft } from "lucide-react";
|
||||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||||
@ -295,6 +296,50 @@ function RecipeModal({ recipe, onClose }: { recipe: Recipe; onClose: () => void
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DailyRecipe {
|
||||||
|
name: string;
|
||||||
|
intro?: string;
|
||||||
|
recipe?: string;
|
||||||
|
seasonNote?: string | null;
|
||||||
|
imageUrl?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RecipeOfTheDay() {
|
||||||
|
const { data } = useQuery<DailyRecipe | null>({
|
||||||
|
queryKey: ["/api/recipes/today"],
|
||||||
|
staleTime: 1000 * 60 * 30,
|
||||||
|
});
|
||||||
|
if (!data || !data.name) return null;
|
||||||
|
return (
|
||||||
|
<section className="mb-10" data-testid="section-recipe-of-day">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="h-px flex-1 bg-card-border" />
|
||||||
|
<h2 className="text-lg font-bold text-foreground px-3">Rezept des Tages</h2>
|
||||||
|
<div className="h-px flex-1 bg-card-border" />
|
||||||
|
</div>
|
||||||
|
<div className="bg-card rounded-xl border border-card-border overflow-hidden md:flex">
|
||||||
|
{data.imageUrl && (
|
||||||
|
<img
|
||||||
|
src={data.imageUrl}
|
||||||
|
alt={data.name}
|
||||||
|
loading="lazy"
|
||||||
|
className="w-full md:w-1/2 aspect-video object-cover"
|
||||||
|
data-testid="img-recipe-of-day"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="p-5 md:w-1/2">
|
||||||
|
<h3 className="text-xl font-bold text-foreground mb-2">{data.name}</h3>
|
||||||
|
{data.intro && <p className="text-sm text-muted-foreground mb-3">{data.intro}</p>}
|
||||||
|
{data.recipe && (
|
||||||
|
<div className="text-sm text-foreground/80 whitespace-pre-line leading-relaxed">{data.recipe}</div>
|
||||||
|
)}
|
||||||
|
{data.seasonNote && <p className="text-xs text-primary mt-3">{data.seasonNote}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function RecipesPage() {
|
export default function RecipesPage() {
|
||||||
usePageMeta("Rezepte - Alpenküche & Volksmusik", "Traditionelle Rezepte aus der Alpenküche und österreichische Hausmannskost. Kochen wie die Volksmusik-Stars bei FOLX TV.");
|
usePageMeta("Rezepte - Alpenküche & Volksmusik", "Traditionelle Rezepte aus der Alpenküche und österreichische Hausmannskost. Kochen wie die Volksmusik-Stars bei FOLX TV.");
|
||||||
const [selectedRecipe, setSelectedRecipe] = useState<Recipe | null>(null);
|
const [selectedRecipe, setSelectedRecipe] = useState<Recipe | null>(null);
|
||||||
@ -320,6 +365,8 @@ export default function RecipesPage() {
|
|||||||
Traditionelle Rezepte aus den germanischen Regionen -- von der Alm bis zur Küste, von der Oma überliefert.
|
Traditionelle Rezepte aus den germanischen Regionen -- von der Alm bis zur Küste, von der Oma überliefert.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<RecipeOfTheDay />
|
||||||
|
|
||||||
{RECIPE_REGIONS.map((region, ri) => (
|
{RECIPE_REGIONS.map((region, ri) => (
|
||||||
<div key={region.name} data-testid={`section-region-${ri}`}>
|
<div key={region.name} data-testid={`section-region-${ri}`}>
|
||||||
<div className="flex items-center gap-3 mb-4 mt-8 first:mt-0">
|
<div className="flex items-center gap-3 mb-4 mt-8 first:mt-0">
|
||||||
|
|||||||
39
server/daily-recipe.ts
Normal file
39
server/daily-recipe.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { db } from "./db";
|
||||||
|
import { dailyRecipes } from "@shared/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
function todayStr(): string {
|
||||||
|
const d = new Date();
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRecipeForToday(): Promise<any | null> {
|
||||||
|
const rows = await db.select().from(dailyRecipes).where(eq(dailyRecipes.dateStr, todayStr()));
|
||||||
|
return rows.length > 0 ? rows[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertRecipe(data: {
|
||||||
|
dateStr?: string;
|
||||||
|
name: string;
|
||||||
|
intro?: string;
|
||||||
|
recipe?: string;
|
||||||
|
seasonNote?: string | null;
|
||||||
|
imageUrl?: string | null;
|
||||||
|
}): Promise<any> {
|
||||||
|
const dateStr = data.dateStr || todayStr();
|
||||||
|
const values = {
|
||||||
|
dateStr,
|
||||||
|
name: data.name,
|
||||||
|
intro: data.intro || "",
|
||||||
|
recipe: data.recipe || "",
|
||||||
|
seasonNote: data.seasonNote || null,
|
||||||
|
imageUrl: data.imageUrl || null,
|
||||||
|
};
|
||||||
|
const existing = await db.select().from(dailyRecipes).where(eq(dailyRecipes.dateStr, dateStr));
|
||||||
|
if (existing.length > 0) {
|
||||||
|
const [row] = await db.update(dailyRecipes).set(values).where(eq(dailyRecipes.id, existing[0].id)).returning();
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
const [row] = await db.insert(dailyRecipes).values(values).returning();
|
||||||
|
return row;
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import { storage } from "./storage";
|
|||||||
import { insertArticleSchema } from "@shared/schema";
|
import { insertArticleSchema } from "@shared/schema";
|
||||||
import { seedDatabase } from "./seed";
|
import { seedDatabase } from "./seed";
|
||||||
import { generateDailyHoroscopes, getHoroscopesForToday, getOrGenerateHoroscope, setHoroscopeImages } from "./horoscope-generator";
|
import { generateDailyHoroscopes, getHoroscopesForToday, getOrGenerateHoroscope, setHoroscopeImages } from "./horoscope-generator";
|
||||||
|
import { getRecipeForToday, upsertRecipe } from "./daily-recipe";
|
||||||
import { startDailyScheduler } from "./scheduler";
|
import { startDailyScheduler } from "./scheduler";
|
||||||
import { analyzeAllArticleImages, getCachedFocalPoints } from "./focal-point";
|
import { analyzeAllArticleImages, getCachedFocalPoints } from "./focal-point";
|
||||||
import { optimizeImage } from "./image-optimizer";
|
import { optimizeImage } from "./image-optimizer";
|
||||||
@ -754,6 +755,26 @@ export async function registerRoutes(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/api/recipes/today", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const recipe = await getRecipeForToday();
|
||||||
|
res.json(recipe);
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(500).json({ message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/recipes", requireApiKey, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name } = req.body || {};
|
||||||
|
if (!name) return res.status(400).json({ message: "name ist erforderlich" });
|
||||||
|
const row = await upsertRecipe(req.body);
|
||||||
|
res.json(row);
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(500).json({ message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/api/horoscopes/today", async (_req, res) => {
|
app.get("/api/horoscopes/today", async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
const horoscopes = await getHoroscopesForToday();
|
const horoscopes = await getHoroscopesForToday();
|
||||||
|
|||||||
@ -34,6 +34,17 @@ export const articleViews = pgTable("article_views", {
|
|||||||
viewedAt: timestamp("viewed_at").notNull().defaultNow(),
|
viewedAt: timestamp("viewed_at").notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const dailyRecipes = pgTable("daily_recipes", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
dateStr: varchar("date_str", { length: 10 }).notNull(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
intro: text("intro").notNull().default(""),
|
||||||
|
recipe: text("recipe").notNull().default(""),
|
||||||
|
seasonNote: text("season_note"),
|
||||||
|
imageUrl: text("image_url"),
|
||||||
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
export const dailyHoroscopes = pgTable("daily_horoscopes", {
|
export const dailyHoroscopes = pgTable("daily_horoscopes", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
signIndex: integer("sign_index").notNull(),
|
signIndex: integer("sign_index").notNull(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user