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:
parent
3a8f58682d
commit
dc00f0c060
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 |
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
122
src/app/(frontend)/posts/page/page.tsx
Normal file
122
src/app/(frontend)/posts/page/page.tsx
Normal 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',
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user