feat: cookie banner full-width, posts listing + detail redesign, richtext fix

- CookieConsent: full-width edge-to-edge bar, no rounded corners
- SiteSettings: acceptedDays/declinedDays fields for configurable consent expiry
- layout.tsx: clean single siteSettings fetch, proper CookieConsent props
- posts/[slug]/page.tsx: redesigned editorial layout matching PDF mockup
- posts/page.tsx: new FD-styled card grid with hero image, summary, date
- RichText: fixed prop from content= to data=, added fd-prose className
This commit is contained in:
Jeffrey 2026-02-18 15:28:39 +01:00
parent 3a8f58682d
commit dc00f0c060
7 changed files with 260 additions and 103 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -8,13 +8,10 @@ import { generateMeta } from '@/utilities/generateMeta'
import { formatDateTime } from '@/utilities/formatDateTime' import { formatDateTime } from '@/utilities/formatDateTime'
import RichText from '@/components/RichText' import RichText from '@/components/RichText'
// ─── Types ────────────────────────────────────────────────────────────────────
type Args = { type Args = {
params: Promise<{ slug: string }> params: Promise<{ slug: string }>
} }
// ─── Page ─────────────────────────────────────────────────────────────────────
export default async function PostPage({ params }: Args) { export default async function PostPage({ params }: Args) {
const { slug } = await params const { slug } = await params
const payload = await getPayload({ config }) const payload = await getPayload({ config })
@ -31,90 +28,84 @@ export default async function PostPage({ params }: Args) {
const heroImage = post.heroImage as Media | undefined const heroImage = post.heroImage as Media | undefined
const authors = (post.populatedAuthors as any[]) ?? [] const authors = (post.populatedAuthors as any[]) ?? []
const summary = post.meta?.description ?? null
return ( return (
<article> <article className="min-h-screen bg-white">
{/* ── Hero ── */}
<section className="relative w-full bg-fd-navy overflow-hidden"> {/* ── Main column ── */}
{/* Background image with navy overlay */} <div className="max-w-[1200px] mx-auto px-6 md:px-8 py-12 md:py-16 lg:py-20">
{heroImage?.url && ( <div className="max-w-[640px]">
<>
<div className="absolute inset-0"> {/* Back link */}
<FDImage <a
image={heroImage} href="/posts"
alt="" className="inline-flex items-center gap-2 font-joey text-sm text-fd-navy/50 hover:text-fd-navy transition-colors mb-8"
className="w-full h-full object-cover opacity-30" >
/> Tillbaka till nyheter
</div> </a>
<div className="absolute inset-0 bg-gradient-to-t from-fd-navy via-fd-navy/80 to-fd-navy/40" />
</> {/* Title */}
<h1 className="font-joey-heavy text-[2rem] md:text-[2.5rem] lg:text-[2.875rem] leading-[1.1] text-fd-navy mb-4">
{post.title}
</h1>
{/* Summary / excerpt (from meta description) */}
{summary && (
<p className="font-joey text-base md:text-lg text-fd-navy/70 leading-relaxed mb-8">
{summary}
</p>
)} )}
<div className="relative max-w-[1200px] mx-auto px-6 md:px-8 pt-16 pb-12 md:pt-24 md:pb-16"> {/* Hero image */}
{/* Category / back link */} {heroImage?.url && (
<a <figure className="mb-6">
href="/posts" <div className="rounded-[20px] overflow-hidden">
className="inline-flex items-center gap-2 font-joey text-sm text-white/60 hover:text-fd-yellow transition-colors mb-8"
>
Tillbaka till nyheter
</a>
{/* Title */}
<h1 className="font-joey-heavy text-4xl md:text-5xl lg:text-[56px] leading-tight text-fd-yellow max-w-[820px] mb-6">
{post.title}
</h1>
{/* Meta row */}
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 font-joey text-sm text-white/70">
{post.publishedAt && (
<span>{formatDateTime(post.publishedAt)}</span>
)}
{authors.length > 0 && (
<span>
Av{' '}
{authors.map((a: any) => a.name).join(', ')}
</span>
)}
</div>
</div>
</section>
{/* ── Hero image (if present) — shown below hero on larger posts ── */}
{heroImage?.url && (
<div className="w-full bg-fd-navy">
<div className="max-w-[1200px] mx-auto px-6 md:px-8 pb-0">
<div className="rounded-[40px] overflow-hidden aspect-[16/7]">
<FDImage <FDImage
image={heroImage} media={heroImage}
alt={heroImage.alt || post.title} size="large"
className="w-full h-full object-cover" className="w-full h-auto object-cover"
priority
/> />
</div> </div>
</div> {heroImage.alt && heroImage.alt.trim() !== '' && (
</div> <figcaption className="mt-3 font-joey text-sm text-fd-navy/50 leading-snug">
)} {heroImage.alt}
</figcaption>
)}
</figure>
)}
{/* ── Article body ── */} {/* Author + date */}
<section className="w-full bg-white py-12 md:py-16 lg:py-20"> <div className="mb-8 font-joey text-sm">
<div className="max-w-[800px] mx-auto px-6 md:px-8"> {authors.length > 0 && (
<div className="prose prose-lg max-w-none fd-prose"> <p className="font-joey-bold text-fd-navy">
{/* Payload rich text renderer */} Av {authors.map((a: any) => a.name).join(', ')}
{post.content && <RichText content={post.content} />} </p>
</div> )}
{post.publishedAt && (
<p className="text-fd-navy/50">{formatDateTime(post.publishedAt)}</p>
)}
</div> </div>
</section>
{/* ── Footer CTA ── */} {/* Rich text body */}
<section className="w-full bg-fd-gray-light py-12 md:py-16"> {post.content && (
<div className="max-w-[1200px] mx-auto px-6 md:px-8 text-center"> <div className="mb-12">
<p className="font-joey text-fd-body-lg text-fd-navy mb-6"> <RichText
Vill du veta mer om hur Fiber Direkt kan hjälpa er verksamhet? data={post.content as any}
</p> enableGutter={false}
<a href="/kontakt" className="fd-btn-primary"> className="fd-prose"
Kontakta oss />
</a> </div>
</div> )}
</section>
{/* CTA */}
<a href="/kontakt" className="fd-btn-primary">
Kontakta oss
</a>
</div>
</div>
</article> </article>
) )
} }

View File

@ -1,16 +1,15 @@
import type { Metadata } from 'next/types' import type { Metadata } from 'next/types'
import { CollectionArchive } from '@/components/CollectionArchive'
import { PageRange } from '@/components/PageRange'
import { Pagination } from '@/components/Pagination'
import configPromise from '@payload-config' import configPromise from '@payload-config'
import { getPayload } from 'payload' import { getPayload } from 'payload'
import React from 'react' import React from 'react'
import type { Post, Media } from '@/payload-types'
import { FDImage } from '@/components/FDImage'
import { formatDateTime } from '@/utilities/formatDateTime'
import { Pagination } from '@/components/Pagination'
import PageClient from './page.client' import PageClient from './page.client'
export const dynamic = 'force-dynamic' export const dynamic = 'force-dynamic'
export default async function Page() { export default async function Page() {
const payload = await getPayload({ config: configPromise }) const payload = await getPayload({ config: configPromise })
@ -22,34 +21,78 @@ export default async function Page() {
select: { select: {
title: true, title: true,
slug: true, slug: true,
heroImage: true,
categories: true, categories: true,
meta: true, meta: true,
publishedAt: true,
populatedAuthors: true,
}, },
}) })
return ( return (
<div className="pt-24 pb-24"> <div className="min-h-screen bg-white">
<PageClient /> <PageClient />
<div className="container mb-16"> <div className="max-w-[1200px] mx-auto px-6 md:px-8 pt-12 md:pt-16 pb-8 md:pb-12">
<div className="prose dark:prose-invert max-w-none"> <h1 className="font-joey-heavy text-4xl md:text-5xl text-fd-navy">Nyheter</h1>
<h1>Posts</h1> <p className="font-joey text-fd-body-lg text-fd-navy/60 mt-3">
</div> {posts.totalDocs} {posts.totalDocs === 1 ? 'artikel' : 'artiklar'}
</p>
</div> </div>
<div className="max-w-[1200px] mx-auto px-6 md:px-8 pb-16 md:pb-24">
<div className="container mb-8"> {posts.docs.length === 0 ? (
<PageRange <p className="font-joey text-fd-navy/50 py-16">Inga inlägg hittades.</p>
collection="posts" ) : (
currentPage={posts.page} <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
limit={12} {posts.docs.map((post) => {
totalDocs={posts.totalDocs} const p = post as Post
/> const heroImage = p.heroImage as Media | undefined
</div> const summary = p.meta?.description ?? null
return (
<CollectionArchive posts={posts.docs} /> <a
key={p.id}
<div className="container"> href={`/posts/${p.slug}`}
className="group flex flex-col bg-white rounded-[20px] border border-fd-navy/10 overflow-hidden hover:border-fd-navy/20 hover:shadow-lg transition-all duration-300"
>
<div className="relative aspect-[16/9] bg-fd-navy/5 overflow-hidden">
{heroImage?.url ? (
<FDImage
media={heroImage}
size="medium"
fill
className="object-cover group-hover:scale-[1.03] transition-transform duration-500"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
fallbackAlt={p.title}
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-fd-navy/5">
<span className="font-joey text-fd-navy/30 text-sm">Ingen bild</span>
</div>
)}
</div>
<div className="flex flex-col flex-1 p-5 md:p-6">
<h2 className="font-joey-bold text-fd-navy text-lg md:text-xl leading-snug mb-2 group-hover:text-fd-navy/80 transition-colors">
{p.title}
</h2>
{summary && (
<p className="font-joey text-sm text-fd-navy/60 leading-relaxed line-clamp-3 mb-4">
{summary}
</p>
)}
{p.publishedAt && (
<p className="font-joey text-xs text-fd-navy/40 mt-auto pt-3 border-t border-fd-navy/8">
{formatDateTime(p.publishedAt)}
</p>
)}
</div>
</a>
)
})}
</div>
)}
{posts.totalPages > 1 && posts.page && ( {posts.totalPages > 1 && posts.page && (
<Pagination page={posts.page} totalPages={posts.totalPages} /> <div className="mt-12">
<Pagination page={posts.page} totalPages={posts.totalPages} />
</div>
)} )}
</div> </div>
</div> </div>
@ -58,6 +101,7 @@ export default async function Page() {
export function generateMetadata(): Metadata { export function generateMetadata(): Metadata {
return { return {
title: `Payload Website Template Posts`, title: 'Nyheter | Fiber Direkt',
description: 'Senaste nytt från Fiber Direkt',
} }
} }

View File

@ -0,0 +1,122 @@
import type { Metadata } from 'next/types'
import configPromise from '@payload-config'
import { getPayload } from 'payload'
import React from 'react'
import type { Post, Media } from '@/payload-types'
import { FDImage } from '@/components/FDImage'
import { formatDateTime } from '@/utilities/formatDateTime'
import { Pagination } from '@/components/Pagination'
import PageClient from './page.client'
export const dynamic = 'force-dynamic'
export default async function Page() {
const payload = await getPayload({ config: configPromise })
const posts = await payload.find({
collection: 'posts',
depth: 1,
limit: 12,
overrideAccess: false,
select: {
title: true,
slug: true,
heroImage: true,
categories: true,
meta: true,
publishedAt: true,
populatedAuthors: true,
},
})
return (
<div className="min-h-screen bg-white">
<PageClient />
{/* ── Page header ── */}
<div className="max-w-[1200px] mx-auto px-6 md:px-8 pt-12 md:pt-16 pb-8 md:pb-12">
<h1 className="font-joey-heavy text-4xl md:text-5xl text-fd-navy">
Nyheter
</h1>
<p className="font-joey text-fd-body-lg text-fd-navy/60 mt-3">
{posts.totalDocs} {posts.totalDocs === 1 ? 'artikel' : 'artiklar'}
</p>
</div>
{/* ── Card grid ── */}
<div className="max-w-[1200px] mx-auto px-6 md:px-8 pb-16 md:pb-24">
{posts.docs.length === 0 ? (
<p className="font-joey text-fd-navy/50 py-16">Inga inlägg hittades.</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
{posts.docs.map((post) => {
const p = post as Post
const heroImage = p.heroImage as Media | undefined
const summary = p.meta?.description ?? null
return (
<a
key={p.id}
href={`/posts/${p.slug}`}
className="group flex flex-col bg-white rounded-[20px] border border-fd-navy/10 overflow-hidden hover:border-fd-navy/20 hover:shadow-lg transition-all duration-300"
>
{/* Image */}
<div className="relative aspect-[16/9] bg-fd-navy/5 overflow-hidden">
{heroImage?.url ? (
<FDImage
media={heroImage}
size="medium"
fill
className="object-cover group-hover:scale-[1.03] transition-transform duration-500"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
fallbackAlt={p.title}
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-fd-navy/5">
<span className="font-joey text-fd-navy/30 text-sm">Ingen bild</span>
</div>
)}
</div>
{/* Content */}
<div className="flex flex-col flex-1 p-5 md:p-6">
<h2 className="font-joey-bold text-fd-navy text-lg md:text-xl leading-snug mb-2 group-hover:text-fd-navy/80 transition-colors">
{p.title}
</h2>
{summary && (
<p className="font-joey text-sm text-fd-navy/60 leading-relaxed line-clamp-3 mb-4">
{summary}
</p>
)}
{/* Date */}
{p.publishedAt && (
<p className="font-joey text-xs text-fd-navy/40 mt-auto pt-3 border-t border-fd-navy/8">
{formatDateTime(p.publishedAt)}
</p>
)}
</div>
</a>
)
})}
</div>
)}
{/* Pagination */}
{posts.totalPages > 1 && posts.page && (
<div className="mt-12">
<Pagination page={posts.page} totalPages={posts.totalPages} />
</div>
)}
</div>
</div>
)
}
export function generateMetadata(): Metadata {
return {
title: 'Nyheter | Fiber Direkt',
description: 'Senaste nytt från Fiber Direkt',
}
}