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:
@@ -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">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user