video kartice v mrezi, oglas le v vsako drugo vrstico in v visini kartice, vremenski pripomocek z naslovnice (prosil je za lokacijo)

This commit is contained in:
2026-08-31 13:31:11 +02:00
parent 7a25bf73cf
commit cc9afe9e28
2 changed files with 76 additions and 12 deletions
+5 -3
View File
@@ -153,12 +153,14 @@ export function ArticleCardAd() {
if (filled === false) return null;
return (
<ArtistPatternBg className="rounded-md border border-card-border h-full bg-card" seed={seed}>
{/* Meritev 31. 8. 2026: kartica clanka je visoka 399 px, fluid in-feed
enota pa se je ustavila pri 248 in pod njo je ostala luknja. Navaden
auto display z minimalno visino zapolni celo celico. */}
<AdSense
slot="3854634730"
format="fluid"
layoutKey="-6r+cy-10+8a-3"
format="auto"
responsive={false}
style={{ display: "block" }}
style={{ display: "block", minHeight: "360px" }}
onAdStatus={setFilled}
/>
</ArtistPatternBg>
+71 -9
View File
@@ -12,7 +12,6 @@ import Header from "@/components/header";
import Footer from "@/components/footer";
import AdSense, { ArticleCardAd, SidebarAd, PageSideAds, MultiplexAd, TopBannerAd } from "@/components/adsense";
import { NewsWidget } from "@/components/news-widget";
import { SidebarWeatherWidget } from "@/components/weather-widget";
import { BreakingNewsWidget } from "@/components/breaking-news-widget";
import PushPromptBanner from "@/components/push-prompt-banner";
import { useState, useEffect, useCallback, useRef, useMemo, lazy, Suspense } from "react";
@@ -332,6 +331,53 @@ function MobileMosaic({ latest, popular, focalPoints }: { latest: Article[]; pop
);
}
interface YtVideo { ytId: string; slug: string; shortTitle?: string; title: string; rubrik?: string; publishedAt?: string; durationS?: number }
// Video kartica v mrezi: enaka velikost kot kartica clanka, da v vrstici ni
// luknje. Pelje na /video/<slug>, kjer je oglasni prostor, ne na YouTube.
function VideoCard({ video }: { video: YtVideo }) {
const mm = Math.floor((video.durationS || 0) / 60);
const ss = String((video.durationS || 0) % 60).padStart(2, "0");
return (
<Link href={`/video/${video.slug}`}>
<div className="relative group cursor-pointer rounded-lg overflow-hidden h-full bg-card border border-card-border flex flex-col" data-testid={`card-video-${video.ytId}`}>
<div className="relative flex-shrink-0">
<img
src={`https://i.ytimg.com/vi/${video.ytId}/hqdefault.jpg`}
alt={video.shortTitle || video.title}
className="w-full aspect-video object-cover transition-transform duration-500 group-hover:scale-105"
loading="lazy"
/>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-10 h-10 rounded-full bg-primary/90 flex items-center justify-center group-hover:bg-primary transition-colors">
<Play className="w-4 h-4 text-white ml-0.5" fill="white" />
</div>
</div>
{video.durationS ? (
<span className="absolute bottom-1.5 right-1.5 rounded bg-black/75 px-1.5 py-0.5 text-[10px] font-medium text-white">
{mm}:{ss}
</span>
) : null}
</div>
<div className="p-3.5 flex flex-col flex-1">
<div className="flex items-center gap-2 mb-1.5">
<span className="text-[10px] font-medium text-primary">VIDEO</span>
{video.publishedAt && (
<span className="text-muted-foreground text-[10px]">{timeAgo(new Date(video.publishedAt))}</span>
)}
</div>
<h3 className="font-semibold text-card-foreground text-sm leading-snug line-clamp-2 group-hover:text-primary transition-colors">
{video.shortTitle || video.title}
</h3>
<p className="text-xs text-muted-foreground mt-1.5 leading-relaxed flex-1">
Jetzt ansehen auf FOLX TV
</p>
</div>
</div>
</Link>
);
}
function MediumCard({ article, focalPoints }: { article: Article; focalPoints?: Record<string, { x: number; y: number }> }) {
const isVideo = article.category === "Video";
const objPos = getObjectPosition(article.coverImage, focalPoints);
@@ -785,6 +831,12 @@ export default function Home() {
queryKey: ["/api/articles/popular"],
});
// Videi v mrezi: gledalca zadrzijo pri nas in odprejo se eno stran z oglasi.
const { data: ytData } = useQuery<{ items: YtVideo[] }>({
queryKey: ["/api/yt-videos?limit=12"],
});
const videos = ytData?.items || [];
const { data: galleryImages } = useQuery<GalleryImage[]>({
queryKey: ["/api/gallery", "home"],
queryFn: () => fetch("/api/gallery?limit=20").then((r) => r.json()),
@@ -864,20 +916,29 @@ export default function Home() {
}
}
// Oglas gre le v vsako drugo vrstico. Meritev 31. 8. 2026: od 13 enot na
// naslovnici jih je 9 vrnilo unfilled, torej je mest vec, kot jih Google
// za to stran zapolni. Manj mest z visjo zapolnjenostjo prinese vec.
let adCount = 0;
let vidCount = 0;
const totalRows = items.length / 4;
for (let r = 0; r < totalRows; r++) {
const rowStart = r * 4;
const hasWidget = items.slice(rowStart, rowStart + 4).some((it) => it.type === "widget");
if (!hasWidget) {
// Enako kot na straneh rubrik: v prvi vrstici oglas ni prva celica.
const pos = r === 0 ? 3 : adCount % 2 === 0 ? 0 : 3;
if (hasWidget) continue;
if (r % 2 === 1) {
const pos = r === 1 ? 3 : adCount % 2 === 0 ? 0 : 3;
items[rowStart + pos] = { type: "ad", key: `ad-row-${adCount++}` };
} else if (r > 0 && videos.length) {
// video namesto zadnjega clanka v vrstici brez oglasa
const v = videos[vidCount % videos.length];
vidCount += 1;
items[rowStart + 2] = { type: "video", key: `v-${v.ytId}`, video: v };
}
}
return items;
}, [spaced, widgets]);
}, [spaced, widgets, videos]);
const NA_NASLOVNICI = 48;
const gridRows: (typeof gridItems)[] = useMemo(() => {
@@ -977,7 +1038,6 @@ export default function Home() {
/>
</div>
<div className="lg:col-span-1 space-y-4 lg:flex lg:flex-col">
<SidebarWeatherWidget />
{articles && articles.length > 0 && <TopStoriesList articles={razmakniPoIzvajalcu([...articles].sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime())).slice(0, 8)} className="lg:flex-1 lg:min-h-0 overflow-hidden" />}
<SidebarAd />
</div>
@@ -992,9 +1052,11 @@ export default function Home() {
? <div key={item.key}>{item.widget!.el}</div>
: item.type === "ad"
? <div key={item.key} className="h-full" data-testid={`ad-grid-${item.key}`}><ArticleCardAd /></div>
: item.article
? <MediumCard key={item.key} article={item.article} focalPoints={focalPoints} />
: null
: item.type === "video" && item.video
? <VideoCard key={item.key} video={item.video} />
: item.article
? <MediumCard key={item.key} article={item.article} focalPoints={focalPoints} />
: null
)}
{ri === gridRows.length - 1 && widePickedArticles.length > 0 && (
<div className="sm:col-span-2 lg:col-span-4 grid grid-cols-1 sm:grid-cols-2 gap-4">