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:
@@ -153,12 +153,14 @@ export function ArticleCardAd() {
|
|||||||
if (filled === false) return null;
|
if (filled === false) return null;
|
||||||
return (
|
return (
|
||||||
<ArtistPatternBg className="rounded-md border border-card-border h-full bg-card" seed={seed}>
|
<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
|
<AdSense
|
||||||
slot="3854634730"
|
slot="3854634730"
|
||||||
format="fluid"
|
format="auto"
|
||||||
layoutKey="-6r+cy-10+8a-3"
|
|
||||||
responsive={false}
|
responsive={false}
|
||||||
style={{ display: "block" }}
|
style={{ display: "block", minHeight: "360px" }}
|
||||||
onAdStatus={setFilled}
|
onAdStatus={setFilled}
|
||||||
/>
|
/>
|
||||||
</ArtistPatternBg>
|
</ArtistPatternBg>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import Header from "@/components/header";
|
|||||||
import Footer from "@/components/footer";
|
import Footer from "@/components/footer";
|
||||||
import AdSense, { ArticleCardAd, SidebarAd, PageSideAds, MultiplexAd, TopBannerAd } from "@/components/adsense";
|
import AdSense, { ArticleCardAd, SidebarAd, PageSideAds, MultiplexAd, TopBannerAd } from "@/components/adsense";
|
||||||
import { NewsWidget } from "@/components/news-widget";
|
import { NewsWidget } from "@/components/news-widget";
|
||||||
import { SidebarWeatherWidget } from "@/components/weather-widget";
|
|
||||||
import { BreakingNewsWidget } from "@/components/breaking-news-widget";
|
import { BreakingNewsWidget } from "@/components/breaking-news-widget";
|
||||||
import PushPromptBanner from "@/components/push-prompt-banner";
|
import PushPromptBanner from "@/components/push-prompt-banner";
|
||||||
import { useState, useEffect, useCallback, useRef, useMemo, lazy, Suspense } from "react";
|
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 }> }) {
|
function MediumCard({ article, focalPoints }: { article: Article; focalPoints?: Record<string, { x: number; y: number }> }) {
|
||||||
const isVideo = article.category === "Video";
|
const isVideo = article.category === "Video";
|
||||||
const objPos = getObjectPosition(article.coverImage, focalPoints);
|
const objPos = getObjectPosition(article.coverImage, focalPoints);
|
||||||
@@ -785,6 +831,12 @@ export default function Home() {
|
|||||||
queryKey: ["/api/articles/popular"],
|
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[]>({
|
const { data: galleryImages } = useQuery<GalleryImage[]>({
|
||||||
queryKey: ["/api/gallery", "home"],
|
queryKey: ["/api/gallery", "home"],
|
||||||
queryFn: () => fetch("/api/gallery?limit=20").then((r) => r.json()),
|
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 adCount = 0;
|
||||||
|
let vidCount = 0;
|
||||||
const totalRows = items.length / 4;
|
const totalRows = items.length / 4;
|
||||||
for (let r = 0; r < totalRows; r++) {
|
for (let r = 0; r < totalRows; r++) {
|
||||||
const rowStart = r * 4;
|
const rowStart = r * 4;
|
||||||
const hasWidget = items.slice(rowStart, rowStart + 4).some((it) => it.type === "widget");
|
const hasWidget = items.slice(rowStart, rowStart + 4).some((it) => it.type === "widget");
|
||||||
if (!hasWidget) {
|
if (hasWidget) continue;
|
||||||
// Enako kot na straneh rubrik: v prvi vrstici oglas ni prva celica.
|
if (r % 2 === 1) {
|
||||||
const pos = r === 0 ? 3 : adCount % 2 === 0 ? 0 : 3;
|
const pos = r === 1 ? 3 : adCount % 2 === 0 ? 0 : 3;
|
||||||
items[rowStart + pos] = { type: "ad", key: `ad-row-${adCount++}` };
|
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;
|
return items;
|
||||||
}, [spaced, widgets]);
|
}, [spaced, widgets, videos]);
|
||||||
|
|
||||||
const NA_NASLOVNICI = 48;
|
const NA_NASLOVNICI = 48;
|
||||||
const gridRows: (typeof gridItems)[] = useMemo(() => {
|
const gridRows: (typeof gridItems)[] = useMemo(() => {
|
||||||
@@ -977,7 +1038,6 @@ export default function Home() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:col-span-1 space-y-4 lg:flex lg:flex-col">
|
<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" />}
|
{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 />
|
<SidebarAd />
|
||||||
</div>
|
</div>
|
||||||
@@ -992,6 +1052,8 @@ export default function Home() {
|
|||||||
? <div key={item.key}>{item.widget!.el}</div>
|
? <div key={item.key}>{item.widget!.el}</div>
|
||||||
: item.type === "ad"
|
: item.type === "ad"
|
||||||
? <div key={item.key} className="h-full" data-testid={`ad-grid-${item.key}`}><ArticleCardAd /></div>
|
? <div key={item.key} className="h-full" data-testid={`ad-grid-${item.key}`}><ArticleCardAd /></div>
|
||||||
|
: item.type === "video" && item.video
|
||||||
|
? <VideoCard key={item.key} video={item.video} />
|
||||||
: item.article
|
: item.article
|
||||||
? <MediumCard key={item.key} article={item.article} focalPoints={focalPoints} />
|
? <MediumCard key={item.key} article={item.article} focalPoints={focalPoints} />
|
||||||
: null
|
: null
|
||||||
|
|||||||
Reference in New Issue
Block a user