186 lines
6.4 KiB
TypeScript
186 lines
6.4 KiB
TypeScript
import { CampaignContentCard, type CampaignContentCardData } from '@/components/CampaignContentCard'
|
|
import { CampaignContentCarousel } from '@/components/CampaignContentCarousel'
|
|
import type { Post } from '@/payload-types'
|
|
import {
|
|
formatRelativePostDate,
|
|
getCategoryName,
|
|
getPostCanonicalPath,
|
|
getVisiblePosts,
|
|
} from '@/utilities/posts'
|
|
import { getInstagramFeed, type InstagramPost } from '@/utilities/socialFeed/instagramFeed'
|
|
import {
|
|
formatYouTubeViews,
|
|
getYouTubeFeed,
|
|
type YouTubeVideo,
|
|
} from '@/utilities/socialFeed/youtubeFeed'
|
|
import Link from 'next/link'
|
|
|
|
const CONTENT_SECTION_LIMIT = 5
|
|
|
|
const sectionHeaderLinkClassName =
|
|
'text-sm font-bold text-(--pt-red) underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:ring-(--pt-red) focus-visible:outline-none'
|
|
|
|
type DatedCard = {
|
|
card: CampaignContentCardData
|
|
date: number
|
|
}
|
|
|
|
/** 0 sentinel: a missing date sinks the item to the bottom of the recency merge. */
|
|
const dateTimeOf = (value?: string | null): number => {
|
|
const time = new Date(value ?? '').getTime()
|
|
return Number.isNaN(time) ? 0 : time
|
|
}
|
|
|
|
const byRecencyDesc = (a: DatedCard, b: DatedCard): number => b.date - a.date
|
|
|
|
const toArticleCardData = (post: Post): DatedCard | null => {
|
|
const href = getPostCanonicalPath(post)
|
|
if (!href) return null
|
|
|
|
const cover =
|
|
typeof post.coverImage === 'object' && post.coverImage !== null ? post.coverImage : null
|
|
const categoryName = getCategoryName(post)
|
|
|
|
return {
|
|
card: {
|
|
id: post.id,
|
|
href,
|
|
title: post.title,
|
|
shareKind: 'article' as const,
|
|
meta: [formatRelativePostDate(post.publishedDate), categoryName].filter(Boolean).join(' · '),
|
|
...(cover?.url ? { coverUrl: cover.url, coverAlt: cover.alt ?? undefined } : {}),
|
|
...(post.subtitle ? { subtitle: post.subtitle } : {}),
|
|
},
|
|
date: dateTimeOf(post.publishedDate),
|
|
}
|
|
}
|
|
|
|
const toVideoCardData = (video: YouTubeVideo): DatedCard => ({
|
|
card: {
|
|
id: `yt:${video.id}`,
|
|
href: `https://www.youtube.com/watch?v=${video.id}`,
|
|
title: video.title,
|
|
shareKind: 'video' as const,
|
|
meta: [
|
|
formatRelativePostDate(video.publishedAt),
|
|
video.viewCount != null ? `${formatYouTubeViews(video.viewCount)} visualizações` : null,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' · '),
|
|
...(video.thumbnailUrl ? { coverUrl: video.thumbnailUrl, coverAlt: video.title } : {}),
|
|
external: true,
|
|
},
|
|
date: dateTimeOf(video.publishedAt),
|
|
})
|
|
|
|
const toInstagramCardData = (post: InstagramPost): DatedCard => ({
|
|
card: {
|
|
id: `ig:${post.id}`,
|
|
href: post.permalink,
|
|
title: post.caption ?? 'Publicação no Instagram',
|
|
shareKind: 'instagram' as const,
|
|
meta: formatRelativePostDate(post.timestamp) ?? '',
|
|
coverAspect: 'square',
|
|
...(post.thumbnailUrl
|
|
? { coverUrl: post.thumbnailUrl, coverAlt: post.caption ?? 'Publicação no Instagram' }
|
|
: {}),
|
|
external: true,
|
|
},
|
|
date: dateTimeOf(post.timestamp),
|
|
})
|
|
|
|
/**
|
|
* Campaign home content board: the 5 most recent items across the visible
|
|
* posts (S1), the eligible YouTube feed (S2) and the eligible Instagram feed
|
|
* (S3), newest first, as a 1+4 bento on desktop and a one-per-screen carousel
|
|
* on mobile. Hides entirely when nothing is visible (`isPostVisible`
|
|
* fail-closed — hidden electoral tags never leak) and never breaks when a
|
|
* feed API is down (snapshot or no cards). With the feeds unconfigured it
|
|
* degrades to the S1 articles-only behavior.
|
|
*/
|
|
export const CampaignContentSection = async () => {
|
|
const [visiblePosts, youtubeFeed, instagramFeed] = await Promise.all([
|
|
getVisiblePosts(),
|
|
getYouTubeFeed(),
|
|
getInstagramFeed(),
|
|
])
|
|
const articleCards = visiblePosts
|
|
.map(toArticleCardData)
|
|
.filter((entry): entry is DatedCard => entry !== null)
|
|
const videoCards = (youtubeFeed?.videos ?? []).map(toVideoCardData)
|
|
const instagramCards = (instagramFeed?.posts ?? []).map(toInstagramCardData)
|
|
|
|
const cards = [...articleCards, ...videoCards, ...instagramCards]
|
|
.sort(byRecencyDesc)
|
|
.slice(0, CONTENT_SECTION_LIMIT)
|
|
.map(({ card }) => card)
|
|
|
|
if (!cards.length) return null
|
|
|
|
const [featured, ...rest] = cards
|
|
|
|
return (
|
|
<section
|
|
aria-labelledby="contents-title"
|
|
data-home-section="contents"
|
|
className="border-y border-(--campaign-line) bg-(--campaign-band)"
|
|
>
|
|
<div className="mx-auto w-full max-w-[1160px] px-5 py-12 sm:px-8 lg:px-10 lg:py-16">
|
|
<div className="flex flex-wrap items-end justify-between gap-3">
|
|
<div>
|
|
<p className="campaign-section-eyebrow m-0 font-black tracking-[0.1em] text-(--pt-red) uppercase">
|
|
Acompanhe de perto
|
|
</p>
|
|
<h2
|
|
id="contents-title"
|
|
className="campaign-section-title m-0 mt-1 border-0 p-0 font-black tracking-[-0.02em] text-balance"
|
|
>
|
|
A caminhada, em tempo real
|
|
</h2>
|
|
<p className="campaign-section-copy m-0 mt-1 text-(--campaign-muted)">
|
|
Bastidores, caravanas e as lutas do mandato: conteúdo atualizado, direto das redes.
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap gap-4">
|
|
<Link href="/artigos" className={sectionHeaderLinkClassName}>
|
|
Ver artigos →
|
|
</Link>
|
|
{instagramFeed?.username ? (
|
|
<a
|
|
href={`https://www.instagram.com/${instagramFeed.username}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={sectionHeaderLinkClassName}
|
|
>
|
|
Seguir no Instagram →
|
|
</a>
|
|
) : null}
|
|
{youtubeFeed ? (
|
|
<a
|
|
href={`https://www.youtube.com/channel/${youtubeFeed.channelId}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={sectionHeaderLinkClassName}
|
|
>
|
|
YouTube →
|
|
</a>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 hidden grid-cols-4 gap-4 md:grid">
|
|
<div className="col-span-2 row-span-2">
|
|
<CampaignContentCard card={featured} featured />
|
|
</div>
|
|
{rest.map((card) => (
|
|
<CampaignContentCard key={card.id} card={card} />
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-5 md:hidden">
|
|
<CampaignContentCarousel ariaLabel="Conteúdos recentes" items={cards} />
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|