feat: schema freeze — standardize field names, richText fields, localization, admin labels

This commit is contained in:
Jeffrey 2026-02-18 10:07:14 +01:00
parent 5cf9186ee2
commit 5ea9588223
38 changed files with 458 additions and 846 deletions

View File

@ -1,280 +0,0 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@config '../../../tailwind.config.mjs';
@custom-variant dark (&:is([data-theme='dark'] *));
@custom-variant sm (@media (width >= theme(--breakpoint-sm)));
@custom-variant md (@media (width >= theme(--breakpoint-md)));
@custom-variant lg (@media (width >= theme(--breakpoint-lg)));
@custom-variant xl (@media (width >= theme(--breakpoint-xl)));
@custom-variant 2xl (@media (width >= theme(--breakpoint-2xl)));
@layer base {
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: unset;
font-size: unset;
}
}
@plugin "@tailwindcss/typography";
@source inline("lg:col-span-4");
@source inline("lg:col-span-6");
@source inline("lg:col-span-8");
@source inline("lg:col-span-12");
@source inline("border-border");
@source inline("bg-card");
@source inline("border-error");
@source inline("bg-error/30");
@source inline("border-success");
@source inline("bg-success/30");
@source inline("border-warning");
@source inline("bg-warning/30");
@theme {
--breakpoint-sm: 40rem;
--breakpoint-md: 48rem;
--breakpoint-lg: 64rem;
--breakpoint-xl: 80rem;
--breakpoint-2xl: 86rem;
--font-mono: var(--font-geist-mono);
--font-sans: var(--font-geist-sans);
}
@layer utilities {
.container {
width: 100%;
margin-inline: auto;
padding-inline: 1rem;
}
@variant sm {
.container {
max-width: var(--breakpoint-sm);
}
}
@variant md {
.container {
max-width: var(--breakpoint-md);
padding-inline: 2rem;
}
}
@variant lg {
.container {
max-width: var(--breakpoint-lg);
}
}
@variant xl {
.container {
max-width: var(--breakpoint-xl);
}
}
@variant 2xl {
.container {
max-width: var(--breakpoint-2xl);
}
}
}
:root {
--background: oklch(100% 0 0deg);
--foreground: oklch(14.5% 0 0deg);
--card: oklch(96.5% 0.005 265deg);
--card-foreground: oklch(14.5% 0 0deg);
--popover: oklch(100% 0 0deg);
--popover-foreground: oklch(14.5% 0 0deg);
--primary: oklch(20.5% 0 0deg);
--primary-foreground: oklch(98.5% 0 0deg);
--secondary: oklch(97% 0 0deg);
--secondary-foreground: oklch(20.5% 0 0deg);
--muted: oklch(97% 0 0deg);
--muted-foreground: oklch(55.6% 0 0deg);
--accent: oklch(97% 0 0deg);
--accent-foreground: oklch(20.5% 0 0deg);
--destructive: oklch(57.7% 0.245 27.325deg);
--destructive-foreground: oklch(57.7% 0.245 27.325deg);
--border: oklch(92.2% 0 0deg);
--input: oklch(92.2% 0 0deg);
--ring: oklch(70.8% 0 0deg);
--chart-1: oklch(64.6% 0.222 41.116deg);
--chart-2: oklch(60% 0.118 184.704deg);
--chart-3: oklch(39.8% 0.07 227.392deg);
--chart-4: oklch(82.8% 0.189 84.429deg);
--chart-5: oklch(76.9% 0.188 70.08deg);
--radius: 0.625rem;
--sidebar: oklch(98.5% 0 0deg);
--sidebar-foreground: oklch(14.5% 0 0deg);
--sidebar-primary: oklch(20.5% 0 0deg);
--sidebar-primary-foreground: oklch(98.5% 0 0deg);
--sidebar-accent: oklch(97% 0 0deg);
--sidebar-accent-foreground: oklch(20.5% 0 0deg);
--sidebar-border: oklch(92.2% 0 0deg);
--sidebar-ring: oklch(70.8% 0 0deg);
--success: oklch(78% 0.08 200deg);
--warning: oklch(89% 0.1 75deg);
--error: oklch(75% 0.15 25deg);
}
[data-theme='dark'] {
--background: oklch(14.5% 0 0deg);
--foreground: oklch(98.5% 0 0deg);
--card: oklch(17% 0 0deg);
--card-foreground: oklch(98.5% 0 0deg);
--popover: oklch(14.5% 0 0deg);
--popover-foreground: oklch(98.5% 0 0deg);
--primary: oklch(98.5% 0 0deg);
--primary-foreground: oklch(20.5% 0 0deg);
--secondary: oklch(26.9% 0 0deg);
--secondary-foreground: oklch(98.5% 0 0deg);
--muted: oklch(26.9% 0 0deg);
--muted-foreground: oklch(70.8% 0 0deg);
--accent: oklch(26.9% 0 0deg);
--accent-foreground: oklch(98.5% 0 0deg);
--destructive: oklch(39.6% 0.141 25.723deg);
--destructive-foreground: oklch(63.7% 0.237 25.331deg);
--border: oklch(26.9% 0 0deg);
--input: oklch(26.9% 0 0deg);
--ring: oklch(43.9% 0 0deg);
--chart-1: oklch(48.8% 0.243 264.376deg);
--chart-2: oklch(69.6% 0.17 162.48deg);
--chart-3: oklch(76.9% 0.188 70.08deg);
--chart-4: oklch(62.7% 0.265 303.9deg);
--chart-5: oklch(64.5% 0.246 16.439deg);
--sidebar: oklch(20.5% 0 0deg);
--sidebar-foreground: oklch(98.5% 0 0deg);
--sidebar-primary: oklch(48.8% 0.243 264.376deg);
--sidebar-primary-foreground: oklch(98.5% 0 0deg);
--sidebar-accent: oklch(26.9% 0 0deg);
--sidebar-accent-foreground: oklch(98.5% 0 0deg);
--sidebar-border: oklch(26.9% 0 0deg);
--sidebar-ring: oklch(43.9% 0 0deg);
--success: oklch(28% 0.1 200deg);
--warning: oklch(35% 0.08 70deg);
--error: oklch(45% 0.1 25deg);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--color-success: var(--success);
--color-warning: var(--warning);
--color-error: var(--error);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground min-h-[100vh] flex flex-col;
font-variant-ligatures: none;
}
}
html {
opacity: 0;
}
html[data-theme='dark'],
html[data-theme='light'] {
opacity: initial;
}
/* ============================================
FIBER DIREKT ADDITIONS
Append this entire block to the END of:
src/app/(frontend)/globals.css
============================================ */
/* FS Joey Font Faces — place .otf files in public/fonts/ */
@font-face {
font-family: 'FS Joey';
src: url('/fonts/fs-joey-regular.otf') format('opentype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'FS Joey Medium';
src: url('/fonts/fs-joey-medium.otf') format('opentype');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'FS Joey Bold';
src: url('/fonts/fs-joey-bold.otf') format('opentype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'FS Joey Heavy';
src: url('/fonts/fs-joey-heavy.otf') format('opentype');
font-weight: 900;
font-style: normal;
font-display: swap;
}
/* Fiber Direkt design tokens */
@theme {
--color-fd-navy: #0e2338;
--color-fd-navy-light: #0f2339;
--color-fd-yellow: #fecc02;
--color-fd-gray: #f3f3f3;
--color-fd-gray-light: #f7f7f7;
--font-joey: 'FS Joey', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
--font-joey-medium: 'FS Joey Medium', 'FS Joey', system-ui, sans-serif;
--font-joey-bold: 'FS Joey Bold', 'FS Joey', system-ui, sans-serif;
--font-joey-heavy: 'FS Joey Heavy', 'FS Joey', system-ui, sans-serif;
}

View File

@ -244,6 +244,14 @@ html[data-theme='light'] {
font-display: swap;
}
@font-face {
font-family: 'FS Joey';
src: url('/fonts/fs-joey-italic.otf') format('opentype');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'FS Joey Medium';
src: url('/fonts/fs-joey-medium.otf') format('opentype');
@ -278,6 +286,7 @@ html[data-theme='light'] {
@theme {
/* ---- Fonts ---- */
--font-joey: 'FS Joey', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
--font-joey-italic: 'FS Joey', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
--font-joey-medium: 'FS Joey Medium', 'FS Joey', system-ui, sans-serif;
--font-joey-bold: 'FS Joey Bold', 'FS Joey', system-ui, sans-serif;
--font-joey-heavy: 'FS Joey Heavy', 'FS Joey', system-ui, sans-serif;
@ -680,3 +689,81 @@ html[data-theme='light'] {
--fd-border-strong: #245580;
}
}
/* FD Prose styles (for Posts rich text)
Add these to your existing globals.css
*/
.fd-prose {
font-family: var(--font-joey, sans-serif);
color: theme('colors.fd-navy');
}
.fd-prose h1,
.fd-prose h2,
.fd-prose h3,
.fd-prose h4 {
font-family: var(--font-joey-heavy, sans-serif);
color: theme('colors.fd-navy');
margin-top: 2rem;
margin-bottom: 1rem;
}
.fd-prose h2 { font-size: 1.75rem; }
.fd-prose h3 { font-size: 1.35rem; }
.fd-prose p {
font-size: 1.125rem;
line-height: 1.75;
margin-bottom: 1.25rem;
}
.fd-prose a {
color: theme('colors.fd-navy');
text-decoration: underline;
text-underline-offset: 3px;
font-family: var(--font-joey-bold, sans-serif);
}
.fd-prose a:hover {
color: theme('colors.fd-yellow');
}
.fd-prose ul,
.fd-prose ol {
padding-left: 1.5rem;
margin-bottom: 1.25rem;
}
.fd-prose li {
font-size: 1.125rem;
line-height: 1.75;
margin-bottom: 0.5rem;
}
.fd-prose ul li::marker {
color: theme('colors.fd-yellow');
}
.fd-prose strong {
font-family: var(--font-joey-bold, sans-serif);
font-weight: normal;
}
.fd-prose blockquote {
border-left: 4px solid theme('colors.fd-yellow');
padding-left: 1.25rem;
margin: 2rem 0;
font-size: 1.2rem;
color: theme('colors.fd-navy');
font-style: italic;
}
.fd-prose img {
border-radius: 24px;
margin: 2rem 0;
}
.fd-prose hr {
border-color: #e5e5e5;
margin: 2.5rem 0;
}

View File

@ -1,88 +1,150 @@
export const dynamic = 'force-dynamic'
import type { Metadata } from 'next'
import { RelatedPosts } from '@/blocks/RelatedPosts/Component'
import { PayloadRedirects } from '@/components/PayloadRedirects'
import configPromise from '@payload-config'
import { notFound } from 'next/navigation'
import { getPayload } from 'payload'
import { draftMode } from 'next/headers'
import React, { cache } from 'react'
import RichText from '@/components/RichText'
import type { Post } from '@/payload-types'
import { PostHero } from '@/heros/PostHero'
import config from '@payload-config'
import type { Post, Media } from '@/payload-types'
import { FDImage } from '@/components/FDImage'
import { generateMeta } from '@/utilities/generateMeta'
import PageClient from './page.client'
import { LivePreviewListener } from '@/components/LivePreviewListener'
import { formatDate } from '@/utilities/formatDate'
// ─── Types ────────────────────────────────────────────────────────────────────
type Args = {
params: Promise<{
slug?: string
}>
params: Promise<{ slug: string }>
}
export default async function Post({ params: paramsPromise }: Args) {
const { isEnabled: draft } = await draftMode()
const { slug = '' } = await paramsPromise
// Decode to support slugs with special characters
const decodedSlug = decodeURIComponent(slug)
const url = '/posts/' + decodedSlug
const post = await queryPostBySlug({ slug: decodedSlug })
// ─── Page ─────────────────────────────────────────────────────────────────────
export default async function PostPage({ params }: Args) {
const { slug } = await params
const payload = await getPayload({ config })
if (!post) return <PayloadRedirects url={url} />
const result = await payload.find({
collection: 'posts',
where: { slug: { equals: slug } },
depth: 2,
limit: 1,
})
const post = result.docs?.[0] as Post | undefined
if (!post) notFound()
const heroImage = post.heroImage as Media | undefined
const authors = (post.populatedAuthors as any[]) ?? []
return (
<article className="pt-16 pb-16">
<PageClient />
<article>
{/* ── Hero ── */}
<section className="relative w-full bg-fd-navy overflow-hidden">
{/* Background image with navy overlay */}
{heroImage?.url && (
<>
<div className="absolute inset-0">
<FDImage
image={heroImage}
alt=""
className="w-full h-full object-cover opacity-30"
/>
</div>
<div className="absolute inset-0 bg-gradient-to-t from-fd-navy via-fd-navy/80 to-fd-navy/40" />
</>
)}
{/* Allows redirects for valid pages too */}
<PayloadRedirects disableNotFound url={url} />
<div className="relative max-w-[1200px] mx-auto px-6 md:px-8 pt-16 pb-12 md:pt-24 md:pb-16">
{/* Category / back link */}
<a
href="/posts"
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>
{draft && <LivePreviewListener />}
{/* 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>
<PostHero post={post} />
<div className="flex flex-col items-center gap-4 pt-8">
<div className="container">
<RichText className="max-w-[48rem] mx-auto" data={post.content} enableGutter={false} />
{post.relatedPosts && post.relatedPosts.length > 0 && (
<RelatedPosts
className="mt-12 max-w-[52rem] lg:grid lg:grid-cols-subgrid col-start-1 col-span-3 grid-rows-[2fr]"
docs={post.relatedPosts.filter((post) => typeof post === 'object')}
/>
)}
{/* 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>{formatDate(post.publishedAt)}</span>
)}
{authors.length > 0 && (
<span>
Av{' '}
{authors.map((a: any) => a.name).join(', ')}
</span>
)}
</div>
</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
image={heroImage}
alt={heroImage.alt || post.title}
className="w-full h-full object-cover"
/>
</div>
</div>
</div>
)}
{/* ── Article body ── */}
<section className="w-full bg-white py-12 md:py-16 lg:py-20">
<div className="max-w-[800px] mx-auto px-6 md:px-8">
<div className="prose prose-lg max-w-none fd-prose">
{/* Payload rich text renderer */}
{post.content && <RichText content={post.content} />}
</div>
</div>
</section>
{/* ── Footer CTA ── */}
<section className="w-full bg-fd-gray-light py-12 md:py-16">
<div className="max-w-[1200px] mx-auto px-6 md:px-8 text-center">
<p className="font-joey text-fd-body-lg text-fd-navy mb-6">
Vill du veta mer om hur Fiber Direkt kan hjälpa er verksamhet?
</p>
<a href="/kontakt" className="fd-btn-primary">
Kontakta oss
</a>
</div>
</section>
</article>
)
}
export async function generateMetadata({ params: paramsPromise }: Args): Promise<Metadata> {
const { slug = '' } = await paramsPromise
// Decode to support slugs with special characters
const decodedSlug = decodeURIComponent(slug)
const post = await queryPostBySlug({ slug: decodedSlug })
// ─── Metadata ─────────────────────────────────────────────────────────────────
export async function generateMetadata({ params }: Args): Promise<Metadata> {
const { slug } = await params
const payload = await getPayload({ config })
const result = await payload.find({
collection: 'posts',
where: { slug: { equals: slug } },
depth: 1,
limit: 1,
})
const post = result.docs?.[0] as Post | undefined
if (!post) return {}
return generateMeta({ doc: post })
}
const queryPostBySlug = cache(async ({ slug }: { slug: string }) => {
const { isEnabled: draft } = await draftMode()
const payload = await getPayload({ config: configPromise })
const result = await payload.find({
// ─── Static params ────────────────────────────────────────────────────────────
export async function generateStaticParams() {
const payload = await getPayload({ config })
const posts = await payload.find({
collection: 'posts',
draft,
limit: 1,
overrideAccess: draft,
pagination: false,
where: {
slug: {
equals: slug,
},
},
draft: false,
limit: 1000,
overrideAccess: false,
select: { slug: true },
})
return result.docs?.[0] || null
})
return posts.docs?.map(({ slug }) => ({ slug })) ?? []
}

View File

@ -9,35 +9,41 @@ export const FDAlternateHeroBlock: Block = {
},
fields: [
{
name: 'title',
name: 'heading',
type: 'text',
localized: true,
required: true,
label: 'Rubrik',
},
{
name: 'description',
type: 'textarea',
localized: true,
label: 'Beskrivning (valfri)',
},
{
name: 'primaryCtaText',
type: 'text',
localized: true,
label: 'Primär CTA-knapp text (valfri)',
},
{
name: 'primaryCtaLink',
type: 'text',
localized: true,
label: 'Primär CTA-knapp länk',
defaultValue: '/kontakt',
},
{
name: 'secondaryCtaText',
type: 'text',
localized: true,
label: 'Sekundär CTA-knapp text (valfri)',
},
{
name: 'secondaryCtaLink',
type: 'text',
localized: true,
label: 'Sekundär CTA-knapp länk',
defaultValue: '#',
},
@ -53,6 +59,7 @@ export const FDAlternateHeroBlock: Block = {
{
name: 'imageCaption',
type: 'text',
localized: true,
label: 'Bildtext (valfri)',
admin: {
condition: (_, siblingData) => Boolean(siblingData?.image),

View File

@ -4,8 +4,8 @@ export const FDCardGridBlock: Block = {
slug: 'fdCardGrid',
interfaceName: 'FDCardGridBlock',
labels: {
singular: 'FD Card Grid',
plural: 'FD Card Grid',
singular: 'FD Kortrutnät',
plural: 'FD Kortrutnät',
},
fields: [
{
@ -59,11 +59,13 @@ export const FDCardGridBlock: Block = {
{
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik (valfri)',
},
{
name: 'centeredBodyText',
type: 'textarea',
localized: true,
label: 'Centrerad brödtext',
admin: {
condition: (_, siblingData) => siblingData?.displayMode === 'centeredBody',
@ -82,6 +84,7 @@ export const FDCardGridBlock: Block = {
{
name: 'text',
type: 'text',
localized: true,
label: 'Text',
required: true,
},
@ -110,6 +113,7 @@ export const FDCardGridBlock: Block = {
{
name: 'cardLink',
type: 'text',
localized: true,
label: 'Kortlänk (valfri)',
admin: {
description: 'Gör hela kortet klickbart',

View File

@ -12,6 +12,7 @@ export const FDCodeEmbedBlock: Block = {
{
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik (valfri)',
admin: {
description: 'Visas ovanför den inbäddade koden',
@ -20,6 +21,7 @@ export const FDCodeEmbedBlock: Block = {
{
name: 'description',
type: 'textarea',
localized: true,
label: 'Beskrivning (valfri)',
admin: {
description: 'Visas mellan rubrik och inbäddning',
@ -50,6 +52,7 @@ export const FDCodeEmbedBlock: Block = {
{
name: 'iframeTitle',
type: 'text',
localized: true,
label: 'Iframe titel (tillgänglighet)',
defaultValue: 'Inbäddat formulär',
admin: {

View File

@ -4,13 +4,14 @@ export const FDContactBlock: Block = {
slug: 'fdContact',
interfaceName: 'FDContactBlock',
labels: {
singular: 'FD Cotact Us',
plural: 'FD Contact Uss',
singular: 'FD Kontaktinfo',
plural: 'FD Kontaktinfo',
},
fields: [
{
name: 'heading',
type: 'text',
localized: true,
required: true,
label: 'Rubrik',
defaultValue: 'Kontakta oss',
@ -25,6 +26,7 @@ export const FDContactBlock: Block = {
{
name: 'label',
type: 'text',
localized: true,
required: true,
label: 'Etikett',
},

View File

@ -22,6 +22,7 @@ export const FDContactFormBlock: Block = {
{
name: 'heading',
type: 'text',
localized: true,
required: true,
label: 'Rubrik',
defaultValue: 'Prata med vårt team',
@ -29,6 +30,7 @@ export const FDContactFormBlock: Block = {
{
name: 'description',
type: 'textarea',
localized: true,
label: 'Beskrivning',
defaultValue:
'Berätta om era mål — vårt team kontaktar er och hjälper er hitta rätt lösning.',
@ -36,6 +38,7 @@ export const FDContactFormBlock: Block = {
{
name: 'submitText',
type: 'text',
localized: true,
label: 'Skicka-knapp text',
defaultValue: 'Skicka förfrågan',
},
@ -45,7 +48,7 @@ export const FDContactFormBlock: Block = {
type: 'row',
fields: [
{
name: 'background',
name: 'sectionBackground',
type: 'select',
label: 'Bakgrund',
defaultValue: 'white',
@ -86,6 +89,7 @@ export const FDContactFormBlock: Block = {
{
name: 'privacyText',
type: 'textarea',
localized: true,
label: 'Integritetstext',
defaultValue:
'Vi använder din kontaktinformation för att svara på din förfrågan och dela detaljer om våra produkter och tjänster. Du kan när som helst avregistrera dig.',
@ -96,6 +100,7 @@ export const FDContactFormBlock: Block = {
{
name: 'privacyLinkText',
type: 'text',
localized: true,
label: 'Integritetslänk text',
defaultValue: 'integritetspolicy',
admin: { width: '50%' },
@ -103,6 +108,7 @@ export const FDContactFormBlock: Block = {
{
name: 'privacyLinkUrl',
type: 'text',
localized: true,
label: 'Integritetslänk URL',
defaultValue: '/integritetspolicy',
admin: { width: '50%' },

View File

@ -4,31 +4,35 @@ export const FDCtaSideImageBlock: Block = {
slug: 'fdCtaSideImage',
interfaceName: 'FDCtaSideImageBlock',
labels: {
singular: 'FD CTA with Image',
plural: 'FD CTA with Images',
singular: 'FD CTA med bild',
plural: 'FD CTA med bild',
},
fields: [
{
name: 'heading',
type: 'text',
localized: true,
required: true,
label: 'Rubrik',
},
{
name: 'body',
type: 'textarea',
localized: true,
required: true,
label: 'Brödtext',
},
{
name: 'ctaText',
type: 'text',
localized: true,
label: 'CTA-knapp text',
defaultValue: 'Läs mer',
},
{
name: 'ctaLink',
type: 'text',
localized: true,
label: 'CTA-knapp länk',
defaultValue: '#',
},

View File

@ -9,13 +9,15 @@ export const FDDataTableBlock: Block = {
},
fields: [
{
name: 'title',
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik (valfri)',
},
{
name: 'description',
type: 'textarea',
localized: true,
label: 'Beskrivning (valfri)',
},
@ -60,6 +62,7 @@ export const FDDataTableBlock: Block = {
{
name: 'text',
type: 'text',
localized: true,
required: true,
label: 'Rubrik',
},
@ -77,6 +80,7 @@ export const FDDataTableBlock: Block = {
{
name: 'cells',
type: 'text',
localized: true,
required: true,
label: 'Cellvärden (kommaseparerade)',
admin: {

View File

@ -1,7 +1,7 @@
'use client'
import React, { useState } from 'react'
import type { FDFaqBlock as FDFaqBlockProps } from '@/payload-types'
import RichText from '@/components/RichText'
export const FDFaqBlockComponent: React.FC<FDFaqBlockProps> = ({
heading,
@ -16,10 +16,10 @@ export const FDFaqBlockComponent: React.FC<FDFaqBlockProps> = ({
: theme === 'gray'
? 'bg-fd-gray-light'
: 'bg-white'
const headingColor = theme === 'dark' ? 'text-fd-yellow' : 'text-fd-navy'
const textColor = theme === 'dark' ? 'text-white' : 'text-fd-navy'
const borderColor = theme === 'dark' ? 'border-white/20' : 'border-fd-navy/10'
const proseColor = theme === 'dark' ? 'text-white/80' : 'text-fd-navy/80'
return (
<section className={`w-full py-16 md:py-20 lg:py-[130px] ${bgClass}`}>
@ -27,7 +27,6 @@ export const FDFaqBlockComponent: React.FC<FDFaqBlockProps> = ({
<h2 className={`w-full max-w-[550px] font-joey-heavy text-fd-h1 ${headingColor}`}>
{heading}
</h2>
<div className="w-full max-w-[1162px]">
{items?.map((item, index) => (
<div key={index} className={`border-b ${borderColor}`}>
@ -52,7 +51,6 @@ export const FDFaqBlockComponent: React.FC<FDFaqBlockProps> = ({
{item.question}
</span>
</button>
<div
className={`grid transition-all duration-200 ease-out ${
openIndex === index
@ -61,13 +59,9 @@ export const FDFaqBlockComponent: React.FC<FDFaqBlockProps> = ({
}`}
>
<div className="overflow-hidden">
<p
className={`font-joey text-fd-body pl-7 md:pl-9 ${
theme === 'dark' ? 'text-white/80' : 'text-fd-navy/80'
}`}
>
{item.answer}
</p>
<div className={`font-joey text-fd-body pl-7 md:pl-9 fd-prose ${proseColor}`}>
<RichText content={item.answer} />
</div>
</div>
</div>
</div>

View File

@ -11,6 +11,7 @@ export const FDFaqBlock: Block = {
{
name: 'heading',
type: 'text',
localized: true,
required: true,
label: 'Rubrik',
defaultValue: 'Vanliga frågor',
@ -24,13 +25,14 @@ export const FDFaqBlock: Block = {
{
name: 'question',
type: 'text',
localized: true,
required: true,
label: 'Fråga',
},
{
name: 'answer',
type: 'textarea',
required: true,
type: 'richText', // ← was textarea — editors need bold, links, lists in FAQ answers
localized: true,
label: 'Svar',
},
],

View File

@ -4,30 +4,34 @@ export const FDFeatureAnnouncementBlock: Block = {
slug: 'fdFeatureAnnouncement',
interfaceName: 'FDFeatureAnnouncementBlock',
labels: {
singular: 'FD Feature Announcement',
plural: 'FD Feature Announcements',
singular: 'FD Funktionsnyhet',
plural: 'FD Funktionsnyheter',
},
fields: [
{
name: 'heading',
type: 'text',
localized: true,
required: true,
label: 'Rubrik',
},
{
name: 'body',
type: 'textarea',
localized: true,
required: true,
label: 'Brödtext',
},
{
name: 'ctaText',
type: 'text',
localized: true,
label: 'CTA-text (valfri)',
},
{
name: 'ctaLink',
type: 'text',
localized: true,
label: 'CTA-länk',
},
{

View File

@ -4,18 +4,20 @@ export const FDHeaderTextImageBlock: Block = {
slug: 'fdHeaderTextImage',
interfaceName: 'FDHeaderTextImageBlock',
labels: {
singular: 'FD Header Text Image',
plural: 'FD Header Text Images',
singular: 'FD Rubrik med bild',
plural: 'FD Rubrik med bild',
},
fields: [
{
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik (valfri)',
},
{
name: 'body',
type: 'textarea',
localized: true,
label: 'Brödtext (valfri)',
},
{
@ -63,7 +65,7 @@ export const FDHeaderTextImageBlock: Block = {
],
},
{
name: 'background',
name: 'sectionBackground',
type: 'select',
label: 'Sektionsbakgrund',
defaultValue: 'white',

View File

@ -11,6 +11,7 @@ export const FDHeroBlock: Block = {
{
name: 'heading',
type: 'text',
localized: true,
required: true,
label: 'Rubrik',
defaultValue: 'Sveriges bästa IT-ekosystem för företag',
@ -18,12 +19,14 @@ export const FDHeroBlock: Block = {
{
name: 'subheading',
type: 'text',
localized: true,
label: 'Underrubrik',
defaultValue: 'Fiber, Backup, Colocation och Cloud',
},
{
name: 'body',
type: 'textarea',
localized: true,
label: 'Brödtext',
defaultValue:
'För företag som väljer Sverige. Vi levererar dedikerad fiber, backup, colocation och cloud allt från en leverantör med svenskt huvudmannaskap.',
@ -31,24 +34,28 @@ export const FDHeroBlock: Block = {
{
name: 'ctaText',
type: 'text',
localized: true,
label: 'CTA-knapp text',
defaultValue: 'Kom igång',
},
{
name: 'ctaLink',
type: 'text',
localized: true,
label: 'CTA-knapp länk',
defaultValue: '/kontakt',
},
{
name: 'secondaryCtaText',
type: 'text',
localized: true,
label: 'Sekundär CTA text',
defaultValue: 'Kontakta oss',
},
{
name: 'secondaryCtaLink',
type: 'text',
localized: true,
label: 'Sekundär CTA länk',
defaultValue: '/kontakt',
},

View File

@ -4,13 +4,14 @@ export const FDIconBarBlock: Block = {
slug: 'fdIconBar',
interfaceName: 'FDIconBarBlock',
labels: {
singular: 'FD Icon Row',
plural: 'FD Icon Rows',
singular: 'FD Ikonrad',
plural: 'FD Ikonrader',
},
fields: [
{
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik (valfri)',
},
{
@ -30,6 +31,7 @@ export const FDIconBarBlock: Block = {
{
name: 'label',
type: 'text',
localized: true,
required: true,
label: 'Etikett',
},
@ -53,7 +55,7 @@ export const FDIconBarBlock: Block = {
],
},
{
name: 'background',
name: 'sectionBackground',
type: 'select',
label: 'Sektionsbakgrund',
defaultValue: 'gray',

View File

@ -9,23 +9,27 @@ export const FDLocationsGridBlock: Block = {
},
fields: [
{
name: 'title',
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik (valfri)',
},
{
name: 'description',
type: 'textarea',
localized: true,
label: 'Beskrivning (valfri)',
},
{
name: 'ctaText',
type: 'text',
localized: true,
label: 'CTA-knapp text (valfri)',
},
{
name: 'ctaLink',
type: 'text',
localized: true,
label: 'CTA-knapp länk',
defaultValue: '/kontakt',
},
@ -46,12 +50,14 @@ export const FDLocationsGridBlock: Block = {
{
name: 'locationName',
type: 'text',
localized: true,
required: true,
label: 'Platsnamn',
},
{
name: 'address',
type: 'text',
localized: true,
label: 'Adress',
},
{

View File

@ -12,12 +12,14 @@ export const FDNewsletterBlock: Block = {
{
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik',
defaultValue: 'Håll dig uppdaterad',
},
{
name: 'description',
type: 'textarea',
localized: true,
label: 'Beskrivning',
defaultValue: 'Prenumerera på vårt nyhetsbrev för att få de senaste nyheterna om fiber, cloud och IT-infrastruktur.',
},
@ -35,18 +37,21 @@ export const FDNewsletterBlock: Block = {
{
name: 'buttonText',
type: 'text',
localized: true,
label: 'Knapptext',
defaultValue: 'Prenumerera',
},
{
name: 'successMessage',
type: 'text',
localized: true,
label: 'Bekräftelsemeddelande',
defaultValue: 'Tack! Du är nu prenumerant.',
},
{
name: 'consentText',
type: 'text',
localized: true,
label: 'Samtycketext',
defaultValue: 'Jag godkänner att mina uppgifter används enligt vår integritetspolicy.',
admin: {

View File

@ -9,8 +9,9 @@ export const FDPartnersLogosBlock: Block = {
},
fields: [
{
name: 'title',
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik (valfri)',
defaultValue: 'Våra partners',
},
@ -30,6 +31,7 @@ export const FDPartnersLogosBlock: Block = {
{
name: 'alt',
type: 'text',
localized: true,
label: 'Alt-text',
admin: {
description: 'Beskrivning av logotypen (tillgänglighet)',

View File

@ -4,13 +4,14 @@ export const FDPricingCardBlock: Block = {
slug: 'fdPricingCard',
interfaceName: 'FDPricingCardBlock',
labels: {
singular: 'FD Price Card',
plural: 'FD Price Cards',
singular: 'FD Priskort',
plural: 'FD Priskort',
},
fields: [
{
name: 'sectionTitle',
type: 'text',
localized: true,
label: 'Blockrubrik (valfri)',
admin: {
description: 'Stor rubrik ovanför korten',
@ -26,6 +27,7 @@ export const FDPricingCardBlock: Block = {
{
name: 'title',
type: 'text',
localized: true,
required: true,
label: 'Titel',
admin: {
@ -35,6 +37,7 @@ export const FDPricingCardBlock: Block = {
{
name: 'subtitle',
type: 'text',
localized: true,
label: 'Undertitel',
admin: {
description: 'T.ex. pris: "från 640 kr/mån"',
@ -43,6 +46,7 @@ export const FDPricingCardBlock: Block = {
{
name: 'description',
type: 'textarea',
localized: true,
label: 'Beskrivning',
},
{
@ -53,6 +57,7 @@ export const FDPricingCardBlock: Block = {
{
name: 'text',
type: 'text',
localized: true,
required: true,
label: 'Punkt',
},
@ -61,6 +66,7 @@ export const FDPricingCardBlock: Block = {
{
name: 'ctaText',
type: 'text',
localized: true,
label: 'CTA-knapp text',
defaultValue: 'Få offert',
},

View File

@ -9,14 +9,16 @@ export const FDServiceChooserBlock: Block = {
},
fields: [
{
name: 'title',
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik',
defaultValue: 'Välj din bransch',
},
{
name: 'description',
type: 'textarea',
localized: true,
label: 'Beskrivning (valfri)',
},
{
@ -29,6 +31,7 @@ export const FDServiceChooserBlock: Block = {
{
name: 'label',
type: 'text',
localized: true,
required: true,
label: 'Kategorinamn',
admin: {
@ -38,6 +41,7 @@ export const FDServiceChooserBlock: Block = {
{
name: 'intro',
type: 'textarea',
localized: true,
label: 'Kategoriintro (valfri)',
},
{
@ -50,12 +54,14 @@ export const FDServiceChooserBlock: Block = {
{
name: 'title',
type: 'text',
localized: true,
required: true,
label: 'Tjänsttitel',
},
{
name: 'description',
type: 'textarea',
localized: true,
label: 'Beskrivning',
},
{

View File

@ -4,13 +4,14 @@ export const FDServicesGridBlock: Block = {
slug: 'fdServicesGrid',
interfaceName: 'FDServicesGridBlock',
labels: {
singular: 'FD Service Grid',
plural: 'FD Service Grids',
singular: 'FD Tjänsterutnät',
plural: 'FD Tjänsterutnät',
},
fields: [
{
name: 'heading',
type: 'text',
localized: true,
required: true,
label: 'Rubrik',
defaultValue: 'Företagstjänster',
@ -25,12 +26,14 @@ export const FDServicesGridBlock: Block = {
{
name: 'title',
type: 'text',
localized: true,
required: true,
label: 'Titel',
},
{
name: 'description',
type: 'textarea',
localized: true,
required: true,
label: 'Beskrivning',
},

View File

@ -4,8 +4,8 @@ export const FDSpacerBlock: Block = {
slug: 'fdSpacer',
interfaceName: 'FDSpacerBlock',
labels: {
singular: 'FD Space',
plural: 'FD Spaces',
singular: 'FD Mellanrum',
plural: 'FD Mellanrum',
},
fields: [
{
@ -21,7 +21,7 @@ export const FDSpacerBlock: Block = {
],
},
{
name: 'background',
name: 'sectionBackground',
type: 'select',
label: 'Bakgrundsfärg',
defaultValue: 'white',

View File

@ -9,8 +9,9 @@ export const FDStatisticsBlock: Block = {
},
fields: [
{
name: 'title',
name: 'heading',
type: 'text',
localized: true,
label: 'Blockrubrik (valfri)',
},
{
@ -23,6 +24,7 @@ export const FDStatisticsBlock: Block = {
{
name: 'number',
type: 'text',
localized: true,
required: true,
label: 'Siffra / värde',
admin: {
@ -32,6 +34,7 @@ export const FDStatisticsBlock: Block = {
{
name: 'label',
type: 'text',
localized: true,
required: true,
label: 'Beskrivning',
admin: {

View File

@ -4,13 +4,14 @@ export const FDTagsBlock: Block = {
slug: 'fdTags',
interfaceName: 'FDTagsBlock',
labels: {
singular: 'FD Tags Row',
plural: 'FD Tags Rows',
singular: 'FD Taggar',
plural: 'FD Taggar',
},
fields: [
{
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik (valfri)',
},
{
@ -23,6 +24,7 @@ export const FDTagsBlock: Block = {
{
name: 'text',
type: 'text',
localized: true,
required: true,
label: 'Text',
},

View File

@ -4,8 +4,8 @@ export const FDTechPropertiesBlock: Block = {
slug: 'fdTechProperties',
interfaceName: 'FDTechPropertiesBlock',
labels: {
singular: 'FD Tech Spec',
plural: 'FD tech Specs',
singular: 'FD Tekniska egenskaper',
plural: 'FD Tekniska egenskaper',
},
fields: [
{
@ -18,6 +18,7 @@ export const FDTechPropertiesBlock: Block = {
{
name: 'category',
type: 'text',
localized: true,
required: true,
label: 'Kategori',
admin: { description: 'T.ex. "Skyddsklass"' },
@ -25,6 +26,7 @@ export const FDTechPropertiesBlock: Block = {
{
name: 'value',
type: 'text',
localized: true,
required: true,
label: 'Värde',
admin: { description: 'T.ex. "3" eller "1,6 MW"' },
@ -32,7 +34,7 @@ export const FDTechPropertiesBlock: Block = {
],
},
{
name: 'background',
name: 'sectionBackground',
type: 'select',
label: 'Bakgrundsfärg',
defaultValue: 'navy',

View File

@ -1,5 +1,6 @@
import React from 'react'
import type { FDTextBlock as FDTextBlockProps } from '@/payload-types'
import RichText from '@/components/RichText'
const bgMap: Record<string, string> = {
white: 'bg-white',
@ -33,10 +34,10 @@ export const FDTextBlockComponent: React.FC<FDTextBlockProps> = ({
body,
alignment = 'left',
textColor = 'navy',
background = 'white',
sectionBackground = 'white',
maxWidth = 'wide',
}) => {
const bg = bgMap[background || 'white']
const bg = bgMap[sectionBackground || 'white']
const align = alignMap[alignment || 'left']
const width = maxWidthMap[maxWidth || 'wide']
const colors = textColorMap[textColor || 'navy']
@ -56,7 +57,9 @@ export const FDTextBlockComponent: React.FC<FDTextBlockProps> = ({
<h2 className={`font-joey-medium text-fd-h1 ${colors.h2}`}>{subheading}</h2>
)}
{body && (
<p className={`font-joey text-fd-body-lg ${colors.body}`}>{body}</p>
<div className={`font-joey text-fd-body-lg fd-prose ${colors.body}`}>
<RichText content={body} />
</div>
)}
</div>
</div>

View File

@ -4,23 +4,26 @@ export const FDTextBlock: Block = {
slug: 'fdText',
interfaceName: 'FDTextBlock',
labels: {
singular: 'FD Text Block',
plural: 'FD Text Blocks',
singular: 'FD Textblock',
plural: 'FD Textblock',
},
fields: [
{
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik',
},
{
name: 'subheading',
type: 'text',
localized: true,
label: 'Underrubrik',
},
{
name: 'body',
type: 'textarea',
type: 'richText', // ← was textarea — this block is specifically for editorial content
localized: true,
label: 'Brödtext',
},
{
@ -46,7 +49,7 @@ export const FDTextBlock: Block = {
],
},
{
name: 'background',
name: 'sectionBackground',
type: 'select',
label: 'Bakgrund',
defaultValue: 'white',

View File

@ -4,13 +4,14 @@ export const FDUspChecklistBlock: Block = {
slug: 'fdUspChecklist',
interfaceName: 'FDUspChecklistBlock',
labels: {
singular: 'FD USP Checklist',
plural: 'FD USP Checklists',
singular: 'FD USP-checklista',
plural: 'FD USP-checklistor',
},
fields: [
{
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik',
required: true,
},
@ -23,6 +24,7 @@ export const FDUspChecklistBlock: Block = {
{
name: 'text',
type: 'text',
localized: true,
required: true,
label: 'Text',
},
@ -56,7 +58,7 @@ export const FDUspChecklistBlock: Block = {
],
},
{
name: 'background',
name: 'sectionBackground',
type: 'select',
label: 'Sektionsbakgrund',
defaultValue: 'white',

View File

@ -1,5 +1,6 @@
import React from 'react'
import type { FDUspTableBlock as FDUspTableBlockProps } from '@/payload-types'
import RichText from '@/components/RichText'
const bgMap: Record<string, string> = {
white: 'bg-white',
@ -37,12 +38,13 @@ export const FDUspTableBlockComponent: React.FC<FDUspTableBlockProps> = ({
heading,
rows,
checkColor = 'navy',
background = 'white',
sectionBackground = 'white',
textColor = 'navy',
}) => {
const bg = bgMap[background || 'white']
const bg = bgMap[sectionBackground || 'white']
const txt = textMap[textColor || 'navy']
const border = borderMap[textColor || 'navy']
const proseOpacity = textColor === 'white' ? 'opacity-80' : 'opacity-80'
return (
<section className={`w-full py-12 md:py-16 lg:py-20 ${bg}`}>
@ -62,9 +64,9 @@ export const FDUspTableBlockComponent: React.FC<FDUspTableBlockProps> = ({
<CheckIcon color={checkColor || 'navy'} />
<span className={`font-joey-bold text-fd-h3 ${txt}`}>{row.title}</span>
</div>
<p className={`font-joey text-fd-body ${txt} opacity-80 md:pl-0 pl-14`}>
{row.description}
</p>
<div className={`font-joey text-fd-body fd-prose ${txt} ${proseOpacity} md:pl-0 pl-14`}>
<RichText content={row.description} />
</div>
</div>
))}
</div>

View File

@ -4,13 +4,14 @@ export const FDUspTableBlock: Block = {
slug: 'fdUspTable',
interfaceName: 'FDUspTableBlock',
labels: {
singular: 'FD USP Table',
plural: 'FD USP Tables',
singular: 'FD USP-tabell',
plural: 'FD USP-tabeller',
},
fields: [
{
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik (valfri)',
},
{
@ -22,14 +23,15 @@ export const FDUspTableBlock: Block = {
{
name: 'title',
type: 'text',
localized: true,
required: true,
label: 'Rubrik',
admin: { description: 'T.ex. "Högsta säkerhet"' },
},
{
name: 'description',
type: 'textarea',
required: true,
type: 'richText', // ← was textarea — USP descriptions benefit from bold terms and inline links
localized: true,
label: 'Beskrivning',
},
],
@ -46,7 +48,7 @@ export const FDUspTableBlock: Block = {
],
},
{
name: 'background',
name: 'sectionBackground',
type: 'select',
label: 'Sektionsbakgrund',
defaultValue: 'white',

View File

@ -12,11 +12,13 @@ export const FDVideoBlock: Block = {
{
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik (valfri)',
},
{
name: 'description',
type: 'textarea',
localized: true,
label: 'Beskrivning (valfri)',
},
// --- Video source ---

View File

@ -1,263 +0,0 @@
'use client'
import React, { useState, useMemo } from 'react'
import type { FDVpsCalculatorBlock as FDVpsCalculatorBlockProps } from '@/payload-types'
const DEFAULT_PRICING = {
windows: 250,
cpuPerCore: 120,
ramPerGb: 100,
ssdPerGb: 4,
hddPerGb: 1,
adminFee: 200,
}
const formatKr = (n: number) => Math.round(n).toLocaleString('sv-SE') + ' kr'
function Toggle({ active, onToggle, isDark }: { active: boolean; onToggle: () => void; isDark: boolean }) {
return (
<button
type="button"
onClick={onToggle}
className={`relative w-14 h-8 rounded-full transition-all duration-300 flex-shrink-0 ${
active ? 'bg-fd-yellow' : isDark ? 'bg-white/20' : 'bg-fd-gray-200'
}`}
role="switch"
aria-checked={active}
>
<div className={`absolute top-1 w-6 h-6 rounded-full bg-white shadow-md transition-all duration-300 ${active ? 'left-7' : 'left-1'}`} />
</button>
)
}
function NumberInput({
label, value, onChange, min = 0, max = 999, step = 1, unit, price, isDark,
}: {
label: string; value: number; onChange: (v: number) => void
min?: number; max?: number; step?: number; unit: string; price: string; isDark: boolean
}) {
const textClass = isDark ? 'text-white' : 'text-fd-navy'
const mutedClass = isDark ? 'text-white/60' : 'text-fd-text-muted'
const inputBg = isDark
? 'bg-white/10 border-white/20 text-white'
: 'bg-fd-surface-alt border-fd-gray-200 text-fd-navy'
const stepBtnClass = isDark
? 'border-[3px] border-white/30 text-white hover:border-white hover:bg-white/10'
: 'border-[3px] border-fd-navy/20 text-fd-navy hover:border-fd-navy hover:bg-fd-navy/5'
return (
<div className="flex items-center justify-between py-3 border-b last:border-0" style={{ borderColor: isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.08)' }}>
<div>
<span className={`font-joey-medium text-fd-body ${textClass}`}>{label}</span>
<span className={`block font-joey text-fd-small ${mutedClass}`}>{price}</span>
</div>
<div className="flex items-center gap-2">
<button type="button" onClick={() => onChange(Math.max(min, value - step))}
className={`w-9 h-9 rounded-full flex items-center justify-center font-joey-bold text-lg transition-all duration-200 ${stepBtnClass}`}></button>
<input
type="number" value={value}
onChange={(e) => onChange(Math.max(min, Math.min(max, Number(e.target.value) || 0)))}
className={`w-20 text-center font-joey-medium text-fd-body rounded-full px-2 py-1.5 border-[3px] ${inputBg} [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none`}
style={{ appearance: 'textfield' }}
/>
<span className={`font-joey text-fd-small ${mutedClass} w-8`}>{unit}</span>
<button type="button" onClick={() => onChange(Math.min(max, value + step))}
className={`w-9 h-9 rounded-full flex items-center justify-center font-joey-bold text-lg transition-all duration-200 ${stepBtnClass}`}>+</button>
</div>
</div>
)
}
export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps> = ({
heading = 'Virtuell server — kalkylator',
description,
contactCtaText = 'Har du frågor? Kontakta oss',
contactCtaLink = '/kontakt',
orderCtaText = 'Beställ',
orderCtaLink = '/kontakt?subject=vps-bestallning',
sectionBackground = 'white',
pricingCpuPerCore,
pricingRamPerGb,
pricingSsdPerGb,
pricingHddPerGb,
pricingWindowsLicense,
discountPercent,
showAdminFee,
adminFeeAmount,
additionalServices = [],
}) => {
const pricing = {
windows: pricingWindowsLicense ?? DEFAULT_PRICING.windows,
cpuPerCore: pricingCpuPerCore ?? DEFAULT_PRICING.cpuPerCore,
ramPerGb: pricingRamPerGb ?? DEFAULT_PRICING.ramPerGb,
ssdPerGb: pricingSsdPerGb ?? DEFAULT_PRICING.ssdPerGb,
hddPerGb: pricingHddPerGb ?? DEFAULT_PRICING.hddPerGb,
}
const discount = (discountPercent ?? 0) / 100
const feeAmount = adminFeeAmount ?? DEFAULT_PRICING.adminFee
const [os, setOs] = useState<'linux' | 'windows'>('linux')
const [cpuCores, setCpuCores] = useState(2)
const [ramGb, setRamGb] = useState(4)
const [ssdGb, setSsdGb] = useState(50)
const [hddGb, setHddGb] = useState(0)
const [addAdminFee, setAddAdminFee] = useState(false)
const [extraToggles, setExtraToggles] = useState<Record<number, boolean>>({})
const toggleExtra = (i: number) => setExtraToggles((prev) => ({ ...prev, [i]: !prev[i] }))
const costs = useMemo(() => {
const disc = (v: number) => v * (1 - discount)
const licenseCost = os === 'windows' ? disc(pricing.windows) : 0
const cpuCost = disc(cpuCores * pricing.cpuPerCore)
const ramCost = disc(ramGb * pricing.ramPerGb)
const ssdCost = disc(ssdGb * pricing.ssdPerGb)
const hddCost = disc(hddGb * pricing.hddPerGb)
const feeCost = addAdminFee && showAdminFee ? feeAmount : 0
const extraCosts = (additionalServices ?? []).map((svc, i) => extraToggles[i] ? (svc.price ?? 0) : 0)
const total = licenseCost + cpuCost + ramCost + ssdCost + hddCost + feeCost + extraCosts.reduce((a, b) => a + b, 0)
return { licenseCost, cpuCost, ramCost, ssdCost, hddCost, feeCost, extraCosts, total }
}, [os, cpuCores, ramGb, ssdGb, hddGb, addAdminFee, extraToggles, pricing, discount, feeAmount, additionalServices, showAdminFee])
const isDark = sectionBackground === 'navy'
const bgClass = isDark ? 'bg-fd-navy' : sectionBackground === 'gray' ? 'bg-fd-surface-alt' : 'bg-white'
const cardBorderColor = isDark ? 'rgba(255,255,255,0.15)' : '#d1d5db'
const cardStyle = { border: `6px solid ${cardBorderColor}`, borderRadius: 'clamp(32px, 5vw, 70px)' }
const dividerColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.08)'
const headingClass = isDark ? 'text-fd-yellow' : 'text-fd-navy'
const textClass = isDark ? 'text-white' : 'text-fd-navy'
const mutedClass = isDark ? 'text-white/60' : 'text-fd-text-muted'
const sectionLabelClass = isDark ? 'text-white/40' : 'text-fd-navy/40'
const toggleBorderActive = 'border-fd-yellow bg-fd-yellow text-fd-navy'
const toggleBorderInactive = isDark ? 'border-white/20 text-white/70 hover:text-white' : 'border-fd-navy/20 text-fd-navy/60 hover:text-fd-navy'
const costRows: { label: string; cost: number }[] = [
...(os === 'windows' ? [{ label: 'Licens (Windows)', cost: costs.licenseCost }] : []),
{ label: `CPU (${cpuCores} ${cpuCores === 1 ? 'kärna' : 'kärnor'})`, cost: costs.cpuCost },
{ label: `RAM (${ramGb} GB)`, cost: costs.ramCost },
{ label: `SSD NVMe (${ssdGb} GB)`, cost: costs.ssdCost },
...(hddGb > 0 ? [{ label: `HDD (${hddGb} GB)`, cost: costs.hddCost }] : []),
...(addAdminFee && showAdminFee ? [{ label: 'Administrationsavgift', cost: costs.feeCost }] : []),
...(additionalServices ?? []).flatMap((svc, i) =>
extraToggles[i] ? [{ label: svc.label ?? 'Tilläggstjänst', cost: costs.extraCosts[i] ?? 0 }] : []
),
]
const hasTillval = showAdminFee || ((additionalServices ?? []).length > 0)
const discStr = (discount > 0) ? ` (${discountPercent}% rabatt)` : ''
return (
<section className={`fd-section ${bgClass}`}>
<div className="fd-container">
<div className="text-center mb-10 md:mb-14">
<h2 className={`font-joey-heavy text-fd-h1 ${headingClass} mb-3`}>{heading}</h2>
{description && (
<p className={`font-joey text-fd-body-lg max-w-[640px] mx-auto ${mutedClass}`}>{description}</p>
)}
{(discountPercent ?? 0) > 0 && (
<span className="inline-block mt-3 px-4 py-1 bg-fd-mint text-fd-navy font-joey-bold text-fd-small rounded-full">
{discountPercent}% rabatt inräknad alla resurser
</span>
)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-10">
{/* Config panel */}
<div className="lg:col-span-3 p-6 md:p-8" style={cardStyle}>
<div className="mb-6">
<label className={`font-joey-medium text-fd-small uppercase tracking-wider block mb-3 ${sectionLabelClass}`}>
Operativsystem
</label>
<div className="flex gap-2">
{(['linux', 'windows'] as const).map((opt) => (
<button key={opt} type="button" onClick={() => setOs(opt)}
className={`flex-1 py-2.5 rounded-full font-joey-bold text-fd-body transition-all duration-300 border-[3px] ${
os === opt ? toggleBorderActive : `bg-transparent ${toggleBorderInactive}`
}`}>
{opt === 'linux' ? 'Linux' : 'Windows'}{' '}
<span className={`text-fd-small font-joey ${os === opt ? 'opacity-60' : 'opacity-40'}`}>
({opt === 'linux' ? 'gratis' : `+${pricing.windows} kr`})
</span>
</button>
))}
</div>
</div>
<div className="space-y-1">
<NumberInput label="CPU-kärnor" value={cpuCores} onChange={setCpuCores} min={1} max={32} unit="st" price={`${pricing.cpuPerCore} kr/kärna${discStr}`} isDark={isDark} />
<NumberInput label="RAM" value={ramGb} onChange={setRamGb} min={1} max={256} unit="GB" price={`${pricing.ramPerGb} kr/GB${discStr}`} isDark={isDark} />
<NumberInput label="SSD NVMe" value={ssdGb} onChange={setSsdGb} min={0} max={4000} step={10} unit="GB" price={`${pricing.ssdPerGb} kr/GB${discStr}`} isDark={isDark} />
<NumberInput label="HDD" value={hddGb} onChange={setHddGb} min={0} max={10000} step={100} unit="GB" price={`${pricing.hddPerGb} kr/GB${discStr}`} isDark={isDark} />
</div>
{hasTillval && (
<div className="mt-6 pt-5" style={{ borderTop: `2px solid ${dividerColor}` }}>
<label className={`font-joey-medium text-fd-small uppercase tracking-wider block mb-4 ${sectionLabelClass}`}>
Tillvalstjänster
</label>
{showAdminFee && (
<div className="flex items-center justify-between py-3 border-b" style={{ borderColor: dividerColor }}>
<div>
<span className={`font-joey-medium text-fd-body ${textClass}`}>Administrationsavgift</span>
<span className={`block font-joey text-fd-small ${mutedClass}`}>{feeAmount} kr/mån</span>
</div>
<Toggle active={addAdminFee} onToggle={() => setAddAdminFee(!addAdminFee)} isDark={isDark} />
</div>
)}
{(additionalServices ?? []).map((svc, i) => (
<div key={i} className="flex items-center justify-between py-3 border-b last:border-0" style={{ borderColor: dividerColor }}>
<div>
<span className={`font-joey-medium text-fd-body ${textClass}`}>{svc.label}</span>
{svc.price != null && (
<span className={`block font-joey text-fd-small ${mutedClass}`}>{svc.price} kr/mån</span>
)}
</div>
<Toggle active={!!extraToggles[i]} onToggle={() => toggleExtra(i)} isDark={isDark} />
</div>
))}
</div>
)}
</div>
{/* Summary panel */}
<div className="lg:col-span-2 p-6 md:p-8 flex flex-col" style={cardStyle}>
<h3 className={`font-joey-medium text-fd-h3 ${headingClass} mb-6`}>Kostnadsöversikt</h3>
<div className="space-y-3 flex-1">
{costRows.length === 0 ? (
<p className={`font-joey text-fd-small ${mutedClass}`}>Konfigurera din server till vänster.</p>
) : costRows.map((row, i) => (
<div key={i} className="flex justify-between items-center">
<span className={`font-joey text-fd-body ${textClass}`}>{row.label}</span>
<span className={`font-joey-medium text-fd-body ${row.cost === 0 ? mutedClass : textClass}`}>
{row.cost === 0 ? 'Gratis' : formatKr(row.cost)}
</span>
</div>
))}
</div>
<div className="mt-6 pt-4" style={{ borderTop: `2px solid ${dividerColor}` }}>
<div className="flex justify-between items-end">
<span className={`font-joey-medium text-fd-body ${textClass}`}>Totalt per månad</span>
<span className={`font-joey-heavy text-fd-h2 ${headingClass}`}>{formatKr(costs.total)}</span>
</div>
<span className={`font-joey text-fd-xs ${mutedClass} block text-right mt-1`}>exkl. moms</span>
</div>
<div className="mt-8 space-y-3">
<a href={orderCtaLink || '#'} className="fd-btn-primary w-full text-center">{orderCtaText}</a>
<a href={contactCtaLink || '#'} className={`${isDark ? 'fd-btn-secondary-dark' : 'fd-btn-secondary'} w-full text-center`}>
{contactCtaText}
</a>
</div>
</div>
</div>
</div>
</section>
)
}

View File

@ -1,135 +0,0 @@
import type { Block } from 'payload'
export const FDVpsCalculatorBlock: Block = {
slug: 'fdVpsCalculator',
interfaceName: 'FDVpsCalculatorBlock',
labels: { singular: 'VPS Kalkylator', plural: 'VPS Kalkylatorer' },
fields: [
// ─── Presentation ──────────────────────────────────────────
{
name: 'heading',
type: 'text',
label: 'Rubrik',
defaultValue: 'Virtuell server — kalkylator',
},
{
name: 'description',
type: 'text',
label: 'Beskrivning',
},
{
type: 'row',
fields: [
{ name: 'orderCtaText', type: 'text', label: 'Beställ-knapp text', defaultValue: 'Beställ' },
{ name: 'orderCtaLink', type: 'text', label: 'Beställ-länk', defaultValue: '/kontakt?subject=vps-bestallning' },
],
},
{
type: 'row',
fields: [
{ name: 'contactCtaText', type: 'text', label: 'Kontakt-knapp text', defaultValue: 'Har du frågor? Kontakta oss' },
{ name: 'contactCtaLink', type: 'text', label: 'Kontakt-länk', defaultValue: '/kontakt' },
],
},
{
name: 'sectionBackground',
type: 'select',
label: 'Bakgrundsfärg',
defaultValue: 'white',
options: [
{ label: 'Vit', value: 'white' },
{ label: 'Grå', value: 'gray' },
{ label: 'Navy', value: 'navy' },
],
},
// ─── Prissättning ──────────────────────────────────────────
{
type: 'collapsible',
label: 'Prissättning (kr/enhet)',
admin: { initCollapsed: true },
fields: [
{
type: 'row',
fields: [
{ name: 'pricingCpuPerCore', type: 'number', label: 'CPU per kärna (kr)', defaultValue: 120, admin: { description: 'Standard: 120 kr/kärna' } },
{ name: 'pricingRamPerGb', type: 'number', label: 'RAM per GB (kr)', defaultValue: 100, admin: { description: 'Standard: 100 kr/GB' } },
],
},
{
type: 'row',
fields: [
{ name: 'pricingSsdPerGb', type: 'number', label: 'SSD NVMe per GB (kr)', defaultValue: 4, admin: { description: 'Standard: 4 kr/GB' } },
{ name: 'pricingHddPerGb', type: 'number', label: 'HDD per GB (kr)', defaultValue: 1, admin: { description: 'Standard: 1 kr/GB' } },
],
},
{
name: 'pricingWindowsLicense',
type: 'number',
label: 'Windows-licens per månad (kr)',
defaultValue: 250,
admin: { description: 'Tillägg för Windows OS. Standard: 250 kr/mån' },
},
],
},
// ─── Rabatt ────────────────────────────────────────────────
{
name: 'discountPercent',
type: 'number',
label: 'Kampanjrabatt (%)',
min: 0,
max: 100,
admin: {
description: 'Procentuell rabatt som appliceras på alla resurser (CPU, RAM, SSD, HDD, Windows). Lämna tomt eller 0 för ingen rabatt.',
step: 1,
},
},
// ─── Tillvalstjänster ──────────────────────────────────────
{
type: 'collapsible',
label: 'Tillvalstjänster',
admin: { initCollapsed: false },
fields: [
// Built-in admin fee toggle
{
type: 'row',
fields: [
{
name: 'showAdminFee',
type: 'checkbox',
label: 'Visa administrationsavgift',
defaultValue: false,
admin: { description: 'Visar en toggle för administrationsavgift i kalkylatorn' },
},
{
name: 'adminFeeAmount',
type: 'number',
label: 'Administrationsavgift (kr/mån)',
defaultValue: 200,
admin: { description: 'Standard: 200 kr/mån', condition: (_, siblingData) => siblingData?.showAdminFee },
},
],
},
// Additional custom services
{
name: 'additionalServices',
type: 'array',
label: 'Fler tillvalstjänster',
admin: { description: 'Lägg till egna tillvalstjänster som syns som toggles i kalkylatorn.' },
fields: [
{ name: 'label', type: 'text', label: 'Tjänstnamn', required: true },
{
name: 'price',
type: 'number',
label: 'Pris (kr/mån)',
required: true,
admin: { description: 'Fast månadskostnad för denna tjänst' },
},
],
},
],
},
],
}

View File

@ -3,7 +3,7 @@ import type { Block } from 'payload'
export const FDVpsCalculatorBlock: Block = {
slug: 'fdVpsCalculator',
interfaceName: 'FDVpsCalculatorBlock',
labels: { singular: 'VPS Kalkylator', plural: 'VPS Kalkylatorer' },
labels: { singular: 'FD VPS-kalkylator', plural: 'FD VPS-kalkylatorer' },
fields: [
// ─── Presentation ──────────────────────────────────────────────────────
{ name: 'heading', type: 'text', label: 'Rubrik', defaultValue: 'Virtuell server — kalkylator' },

View File

@ -4,24 +4,27 @@ export const FDWideCardBlock: Block = {
slug: 'fdWideCard',
interfaceName: 'FDWideCardBlock',
labels: {
singular: 'FD Wide Card',
plural: 'FD Wide Cards',
singular: 'FD Bredt kort',
plural: 'FD Breda kort',
},
fields: [
{
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik',
required: true,
},
{
name: 'body',
type: 'textarea',
localized: true,
label: 'Brödtext (valfri)',
},
{
name: 'ctaText',
type: 'text',
localized: true,
label: 'CTA-knapp text (valfri)',
},
{

View File

@ -518,7 +518,21 @@ export interface FDFaqBlock {
items?:
| {
question: string;
answer: string;
answer?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
id?: string | null;
}[]
| null;
@ -613,7 +627,7 @@ export interface FDPricingCardBlock {
*/
export interface FDSpacerBlock {
height?: ('sm' | 'md' | 'lg' | 'xl') | null;
background?: ('white' | 'navy' | 'gray' | 'yellow' | 'transparent') | null;
sectionBackground?: ('white' | 'navy' | 'gray' | 'yellow' | 'transparent') | null;
id?: string | null;
blockName?: string | null;
blockType: 'fdSpacer';
@ -633,7 +647,7 @@ export interface FDIconBarBlock {
}[]
| null;
iconStyle?: ('navy' | 'yellow' | 'gray' | 'none') | null;
background?: ('white' | 'gray' | 'navy' | 'yellow') | null;
sectionBackground?: ('white' | 'gray' | 'navy' | 'yellow') | null;
textColor?: ('navy' | 'white') | null;
id?: string | null;
blockName?: string | null;
@ -654,7 +668,7 @@ export interface FDUspChecklistBlock {
image?: (number | null) | Media;
imagePosition?: ('right' | 'left') | null;
checkColor?: ('navy' | 'yellow' | 'gray') | null;
background?: ('white' | 'gray' | 'navy') | null;
sectionBackground?: ('white' | 'gray' | 'navy') | null;
textColor?: ('navy' | 'white') | null;
id?: string | null;
blockName?: string | null;
@ -695,7 +709,7 @@ export interface FDTechPropertiesBlock {
id?: string | null;
}[]
| null;
background?: ('navy' | 'white' | 'gray' | 'yellow') | null;
sectionBackground?: ('navy' | 'white' | 'gray' | 'yellow') | null;
categoryColor?: ('white' | 'navy') | null;
valueColor?: ('yellow' | 'white' | 'navy') | null;
id?: string | null;
@ -714,12 +728,26 @@ export interface FDUspTableBlock {
* T.ex. "Högsta säkerhet"
*/
title: string;
description: string;
description?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
id?: string | null;
}[]
| null;
checkColor?: ('navy' | 'yellow' | 'gray') | null;
background?: ('white' | 'gray' | 'navy') | null;
sectionBackground?: ('white' | 'gray' | 'navy') | null;
textColor?: ('navy' | 'white') | null;
id?: string | null;
blockName?: string | null;
@ -738,7 +766,7 @@ export interface FDHeaderTextImageBlock {
| null;
imageRounded?: ('none' | 'medium' | 'large') | null;
textAlign?: ('left' | 'center') | null;
background?: ('white' | 'gray' | 'navy') | null;
sectionBackground?: ('white' | 'gray' | 'navy') | null;
textColor?: ('navy' | 'white') | null;
id?: string | null;
blockName?: string | null;
@ -756,7 +784,7 @@ export interface FDContactFormBlock {
heading: string;
description?: string | null;
submitText?: string | null;
background?: ('white' | 'gray' | 'navy' | 'navyGradient') | null;
sectionBackground?: ('white' | 'gray' | 'navy' | 'navyGradient') | null;
layout?: ('standard' | 'withImage' | 'card') | null;
/**
* Visas till höger om formuläret desktop
@ -962,7 +990,7 @@ export interface Form {
* via the `definition` "FDLocationsGridBlock".
*/
export interface FDLocationsGridBlock {
title?: string | null;
heading?: string | null;
description?: string | null;
ctaText?: string | null;
ctaLink?: string | null;
@ -986,7 +1014,7 @@ export interface FDLocationsGridBlock {
* via the `definition` "FDAlternateHeroBlock".
*/
export interface FDAlternateHeroBlock {
title: string;
heading: string;
description?: string | null;
primaryCtaText?: string | null;
primaryCtaLink?: string | null;
@ -1007,7 +1035,7 @@ export interface FDAlternateHeroBlock {
* via the `definition` "FDStatisticsBlock".
*/
export interface FDStatisticsBlock {
title?: string | null;
heading?: string | null;
stats?:
| {
/**
@ -1032,7 +1060,7 @@ export interface FDStatisticsBlock {
* via the `definition` "FDPartnersLogosBlock".
*/
export interface FDPartnersLogosBlock {
title?: string | null;
heading?: string | null;
logos?:
| {
image: number | Media;
@ -1091,7 +1119,7 @@ export interface FDNewsletterBlock {
* via the `definition` "FDServiceChooserBlock".
*/
export interface FDServiceChooserBlock {
title?: string | null;
heading?: string | null;
description?: string | null;
categories?:
| {
@ -1122,7 +1150,7 @@ export interface FDServiceChooserBlock {
* via the `definition` "FDDataTableBlock".
*/
export interface FDDataTableBlock {
title?: string | null;
heading?: string | null;
description?: string | null;
/**
* Välj om du vill ladda upp en fil eller ange tabelldata manuellt.
@ -1242,10 +1270,24 @@ export interface FDTagsBlock {
export interface FDTextBlock {
heading?: string | null;
subheading?: string | null;
body?: string | null;
body?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
alignment?: ('left' | 'center' | 'right') | null;
textColor?: ('navy' | 'white' | 'yellow') | null;
background?: ('white' | 'navy' | 'gray' | 'yellow') | null;
sectionBackground?: ('white' | 'navy' | 'gray' | 'yellow') | null;
maxWidth?: ('narrow' | 'medium' | 'wide' | 'full') | null;
id?: string | null;
blockName?: string | null;
@ -1808,7 +1850,7 @@ export interface FDPricingCardBlockSelect<T extends boolean = true> {
*/
export interface FDSpacerBlockSelect<T extends boolean = true> {
height?: T;
background?: T;
sectionBackground?: T;
id?: T;
blockName?: T;
}
@ -1827,7 +1869,7 @@ export interface FDIconBarBlockSelect<T extends boolean = true> {
id?: T;
};
iconStyle?: T;
background?: T;
sectionBackground?: T;
textColor?: T;
id?: T;
blockName?: T;
@ -1847,7 +1889,7 @@ export interface FDUspChecklistBlockSelect<T extends boolean = true> {
image?: T;
imagePosition?: T;
checkColor?: T;
background?: T;
sectionBackground?: T;
textColor?: T;
id?: T;
blockName?: T;
@ -1880,7 +1922,7 @@ export interface FDTechPropertiesBlockSelect<T extends boolean = true> {
value?: T;
id?: T;
};
background?: T;
sectionBackground?: T;
categoryColor?: T;
valueColor?: T;
id?: T;
@ -1900,7 +1942,7 @@ export interface FDUspTableBlockSelect<T extends boolean = true> {
id?: T;
};
checkColor?: T;
background?: T;
sectionBackground?: T;
textColor?: T;
id?: T;
blockName?: T;
@ -1916,7 +1958,7 @@ export interface FDHeaderTextImageBlockSelect<T extends boolean = true> {
imageOverlay?: T;
imageRounded?: T;
textAlign?: T;
background?: T;
sectionBackground?: T;
textColor?: T;
id?: T;
blockName?: T;
@ -1930,7 +1972,7 @@ export interface FDContactFormBlockSelect<T extends boolean = true> {
heading?: T;
description?: T;
submitText?: T;
background?: T;
sectionBackground?: T;
layout?: T;
sideImage?: T;
privacyText?: T;
@ -1951,7 +1993,7 @@ export interface FDContactFormBlockSelect<T extends boolean = true> {
* via the `definition` "FDLocationsGridBlock_select".
*/
export interface FDLocationsGridBlockSelect<T extends boolean = true> {
title?: T;
heading?: T;
description?: T;
ctaText?: T;
ctaLink?: T;
@ -1974,7 +2016,7 @@ export interface FDLocationsGridBlockSelect<T extends boolean = true> {
* via the `definition` "FDAlternateHeroBlock_select".
*/
export interface FDAlternateHeroBlockSelect<T extends boolean = true> {
title?: T;
heading?: T;
description?: T;
primaryCtaText?: T;
primaryCtaLink?: T;
@ -1991,7 +2033,7 @@ export interface FDAlternateHeroBlockSelect<T extends boolean = true> {
* via the `definition` "FDStatisticsBlock_select".
*/
export interface FDStatisticsBlockSelect<T extends boolean = true> {
title?: T;
heading?: T;
stats?:
| T
| {
@ -2009,7 +2051,7 @@ export interface FDStatisticsBlockSelect<T extends boolean = true> {
* via the `definition` "FDPartnersLogosBlock_select".
*/
export interface FDPartnersLogosBlockSelect<T extends boolean = true> {
title?: T;
heading?: T;
logos?:
| T
| {
@ -2048,7 +2090,7 @@ export interface FDNewsletterBlockSelect<T extends boolean = true> {
* via the `definition` "FDServiceChooserBlock_select".
*/
export interface FDServiceChooserBlockSelect<T extends boolean = true> {
title?: T;
heading?: T;
description?: T;
categories?:
| T
@ -2075,7 +2117,7 @@ export interface FDServiceChooserBlockSelect<T extends boolean = true> {
* via the `definition` "FDDataTableBlock_select".
*/
export interface FDDataTableBlockSelect<T extends boolean = true> {
title?: T;
heading?: T;
description?: T;
dataSource?: T;
file?: T;
@ -2159,7 +2201,7 @@ export interface FDTextBlockSelect<T extends boolean = true> {
body?: T;
alignment?: T;
textColor?: T;
background?: T;
sectionBackground?: T;
maxWidth?: T;
id?: T;
blockName?: T;

View File

@ -10,7 +10,7 @@ import { Page, Post } from '@/payload-types'
import { getServerSideURL } from '@/utilities/getURL'
const generateTitle: GenerateTitle<Post | Page> = ({ doc }) => {
return doc?.title ? `${doc.title} | Payload Website Template` : 'Payload Website Template'
return doc?.title ? `${doc.title} | Fiber Direkt` : 'Fiber Direkt'
}
const generateURL: GenerateURL<Post | Page> = ({ doc }) => {
@ -47,9 +47,15 @@ export const plugins: Plugin[] = [
docs.reduce((url, doc) => (doc?.slug ? `${url}/${doc.slug}` : url), ''),
}),
seoPlugin({
generateTitle,
generateURL,
}),
generateTitle,
generateURL,
fields: ({ defaultFields }) => {
return defaultFields.map((field) => ({
...field,
localized: true,
}))
},
}),
formBuilderPlugin({
fields: {
payment: false,