Fix hero card aspect ratio and add horoscope generation functionality

Introduce horoscope generation via OpenAI API, including new API endpoints and database schema. Adjust card components in `home.tsx` to use `aspect-[16/9]` for consistent image sizing, resolving previous height stretching issues. Update dependencies in `package.json`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 413891e8-d784-4bea-b9f5-91a5a68316b4
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: ca1aa952-242c-43c1-9e28-47aed39cee1b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/413891e8-d784-4bea-b9f5-91a5a68316b4/nTLKCC5
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sebastjanartic
2026-02-28 20:25:58 +00:00
parent 774a72bc22
commit 308e602c73
27 changed files with 1653 additions and 27 deletions
+4 -4
View File
@@ -42,8 +42,8 @@ function HeroCard({ article }: { article: Article }) {
const isVideo = article.category === "Video";
return (
<Link href={`/article/${article.slug}`}>
<div className="relative group cursor-pointer rounded-lg overflow-hidden h-full" data-testid={`card-hero-${article.id}`}>
<div className="relative h-full min-h-[300px] md:min-h-[380px]">
<div className="relative group cursor-pointer rounded-lg overflow-hidden" data-testid={`card-hero-${article.id}`}>
<div className="relative aspect-[16/9]">
<img src={article.coverImage || "/images/article-1.png"} alt={article.title} className="w-full h-full object-cover absolute inset-0 transition-transform duration-700 group-hover:scale-105" style={{ objectPosition: "center 25%" }} loading="lazy" />
{isVideo && (
<div className="absolute inset-0 flex items-center justify-center z-10">
@@ -77,8 +77,8 @@ function GalleryHeroCard({ images }: { images: GalleryImage[] }) {
return (
<Link href="/gallery">
<div className="relative group cursor-pointer rounded-lg overflow-hidden h-full" data-testid="card-hero-gallery">
<div className="relative h-full min-h-[300px] md:min-h-[380px]">
<div className="relative group cursor-pointer rounded-lg overflow-hidden" data-testid="card-hero-gallery">
<div className="relative aspect-[16/9]">
<img src={images[idx].large || images[idx].thumb} alt={images[idx].fileName} className="w-full h-full object-cover absolute inset-0 transition-opacity duration-1000" style={{ objectPosition: "center 25%" }} loading="lazy" />
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/30 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-5">
+35 -7
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from "react";
import { useQuery } from "@tanstack/react-query";
import { Link, useParams } from "wouter";
import {
Star,
@@ -15,6 +16,7 @@ import {
Moon,
Sun,
ArrowRight,
Loader2,
} from "lucide-react";
import Header from "@/components/header";
import Footer from "@/components/footer";
@@ -23,12 +25,24 @@ import {
SIGNS,
ELEMENT_COLORS,
ASTRO_EVENTS,
getHoroscope,
getHoroscope as getStaticHoroscope,
getRating,
getLuckyNumbers,
getDailyColor,
} from "@/lib/horoscope-data";
interface AIHoroscope {
signIndex: number;
signName: string;
general: string;
love: string;
career: string;
health: string;
tip: string;
weekly: string;
monthly: string;
}
function StarRating({ count }: { count: number }) {
return (
<div className="flex gap-0.5">
@@ -103,7 +117,7 @@ function AstroEventsSection() {
);
}
function SignGrid({ onSelect, selectedIndex }: { onSelect: (i: number) => void; selectedIndex: number | null }) {
function SignGrid({ onSelect, selectedIndex, aiHoroscopes }: { onSelect: (i: number) => void; selectedIndex: number | null; aiHoroscopes: AIHoroscope[] }) {
return (
<section className="mb-10" data-testid="section-sign-grid">
<h2 className="text-lg font-bold text-foreground flex items-center gap-2 mb-4">
@@ -113,7 +127,8 @@ function SignGrid({ onSelect, selectedIndex }: { onSelect: (i: number) => void;
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
{SIGNS.map((sign, i) => {
const ec = ELEMENT_COLORS[sign.element];
const horoscope = getHoroscope(i);
const aiH = aiHoroscopes.find(h => h.signIndex === i);
const horoscope = aiH || getStaticHoroscope(i);
const isSelected = selectedIndex === i;
return (
<button
@@ -145,14 +160,16 @@ function SignGrid({ onSelect, selectedIndex }: { onSelect: (i: number) => void;
);
}
function SignDetail({ signIndex, onNavigate }: { signIndex: number; onNavigate: (i: number) => void }) {
function SignDetail({ signIndex, onNavigate, aiHoroscopes }: { signIndex: number; onNavigate: (i: number) => void; aiHoroscopes: AIHoroscope[] }) {
const [tab, setTab] = useState<"daily" | "weekly" | "monthly">("daily");
const sign = SIGNS[signIndex];
const ec = ELEMENT_COLORS[sign.element];
const horoscope = getHoroscope(signIndex);
const aiH = aiHoroscopes.find(h => h.signIndex === signIndex);
const horoscope = aiH || getStaticHoroscope(signIndex);
const luckyNums = getLuckyNumbers(signIndex);
const dailyColor = getDailyColor(signIndex);
const detailRef = useRef<HTMLDivElement>(null);
const isAI = !!aiH;
const prevIndex = (signIndex - 1 + SIGNS.length) % SIGNS.length;
const nextIndex = (signIndex + 1) % SIGNS.length;
@@ -355,6 +372,10 @@ export default function HoroscopePage() {
const [selected, setSelected] = useState<number | null>(null);
const detailRef = useRef<HTMLDivElement>(null);
const { data: aiHoroscopes = [], isLoading: aiLoading } = useQuery<AIHoroscope[]>({
queryKey: ["/api/horoscopes/today"],
});
useEffect(() => {
if (params.sign) {
const idx = SIGNS.findIndex((s) => s.name.toLowerCase() === params.sign?.toLowerCase());
@@ -388,13 +409,20 @@ export default function HoroscopePage() {
Entdecken Sie, was die Sterne für Sie bereithalten. Aktuelle kosmische Ereignisse und Ihr persönliches Tageshoroskop.
</p>
{aiLoading && (
<div className="flex items-center gap-2 mb-6 text-sm text-primary bg-primary/10 border border-primary/20 rounded-lg px-4 py-2" data-testid="text-horoscope-loading">
<Loader2 className="w-4 h-4 animate-spin" />
Horoskope werden von den Sternen gelesen...
</div>
)}
<AstroEventsSection />
<SignGrid onSelect={handleSelect} selectedIndex={selected} />
<SignGrid onSelect={handleSelect} selectedIndex={selected} aiHoroscopes={aiHoroscopes} />
<div ref={detailRef}>
{selected !== null ? (
<SignDetail signIndex={selected} onNavigate={handleSelect} />
<SignDetail signIndex={selected} onNavigate={handleSelect} aiHoroscopes={aiHoroscopes} />
) : (
<div className="text-center py-16 bg-card rounded-xl border border-card-border" data-testid="text-horoscope-prompt">
<Sparkles className="w-16 h-16 text-primary mx-auto mb-4" />