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-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-face {
font-family: 'FS Joey Medium'; font-family: 'FS Joey Medium';
src: url('/fonts/fs-joey-medium.otf') format('opentype'); src: url('/fonts/fs-joey-medium.otf') format('opentype');
@ -278,6 +286,7 @@ html[data-theme='light'] {
@theme { @theme {
/* ---- Fonts ---- */ /* ---- Fonts ---- */
--font-joey: 'FS Joey', system-ui, -apple-system, BlinkMacSystemFont, sans-serif; --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-medium: 'FS Joey Medium', 'FS Joey', system-ui, sans-serif;
--font-joey-bold: 'FS Joey Bold', '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; --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-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 type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { RelatedPosts } from '@/blocks/RelatedPosts/Component'
import { PayloadRedirects } from '@/components/PayloadRedirects'
import configPromise from '@payload-config'
import { getPayload } from 'payload' import { getPayload } from 'payload'
import { draftMode } from 'next/headers' import config from '@payload-config'
import React, { cache } from 'react' import type { Post, Media } from '@/payload-types'
import RichText from '@/components/RichText' import { FDImage } from '@/components/FDImage'
import type { Post } from '@/payload-types'
import { PostHero } from '@/heros/PostHero'
import { generateMeta } from '@/utilities/generateMeta' import { generateMeta } from '@/utilities/generateMeta'
import PageClient from './page.client' import { formatDate } from '@/utilities/formatDate'
import { LivePreviewListener } from '@/components/LivePreviewListener'
// ─── Types ────────────────────────────────────────────────────────────────────
type Args = { type Args = {
params: Promise<{ params: Promise<{ slug: string }>
slug?: string
}>
} }
export default async function Post({ params: paramsPromise }: Args) { // ─── Page ─────────────────────────────────────────────────────────────────────
const { isEnabled: draft } = await draftMode() export default async function PostPage({ params }: Args) {
const { slug = '' } = await paramsPromise const { slug } = await params
// Decode to support slugs with special characters const payload = await getPayload({ config })
const decodedSlug = decodeURIComponent(slug)
const url = '/posts/' + decodedSlug
const post = await queryPostBySlug({ slug: decodedSlug })
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 ( return (
<article className="pt-16 pb-16"> <article>
<PageClient /> {/* ── Hero ── */}
<section className="relative w-full bg-fd-navy overflow-hidden">
{/* Allows redirects for valid pages too */} {/* Background image with navy overlay */}
<PayloadRedirects disableNotFound url={url} /> {heroImage?.url && (
<>
{draft && <LivePreviewListener />} <div className="absolute inset-0">
<FDImage
<PostHero post={post} /> image={heroImage}
alt=""
<div className="flex flex-col items-center gap-4 pt-8"> className="w-full h-full object-cover opacity-30"
<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')}
/> />
</div>
<div className="absolute inset-0 bg-gradient-to-t from-fd-navy via-fd-navy/80 to-fd-navy/40" />
</>
)}
<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>
{/* 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>{formatDate(post.publishedAt)}</span>
)}
{authors.length > 0 && (
<span>
Av{' '}
{authors.map((a: any) => a.name).join(', ')}
</span>
)} )}
</div> </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> </article>
) )
} }
export async function generateMetadata({ params: paramsPromise }: Args): Promise<Metadata> { // ─── Metadata ─────────────────────────────────────────────────────────────────
const { slug = '' } = await paramsPromise export async function generateMetadata({ params }: Args): Promise<Metadata> {
// Decode to support slugs with special characters const { slug } = await params
const decodedSlug = decodeURIComponent(slug) const payload = await getPayload({ config })
const post = await queryPostBySlug({ slug: decodedSlug })
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 }) return generateMeta({ doc: post })
} }
const queryPostBySlug = cache(async ({ slug }: { slug: string }) => { // ─── Static params ────────────────────────────────────────────────────────────
const { isEnabled: draft } = await draftMode() export async function generateStaticParams() {
const payload = await getPayload({ config })
const payload = await getPayload({ config: configPromise }) const posts = await payload.find({
const result = await payload.find({
collection: 'posts', collection: 'posts',
draft, draft: false,
limit: 1, limit: 1000,
overrideAccess: draft, overrideAccess: false,
pagination: false, select: { slug: true },
where: {
slug: {
equals: slug,
},
},
}) })
return result.docs?.[0] || null return posts.docs?.map(({ slug }) => ({ slug })) ?? []
}) }

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@ export const FDContactFormBlock: Block = {
{ {
name: 'heading', name: 'heading',
type: 'text', type: 'text',
localized: true,
required: true, required: true,
label: 'Rubrik', label: 'Rubrik',
defaultValue: 'Prata med vårt team', defaultValue: 'Prata med vårt team',
@ -29,6 +30,7 @@ export const FDContactFormBlock: Block = {
{ {
name: 'description', name: 'description',
type: 'textarea', type: 'textarea',
localized: true,
label: 'Beskrivning', label: 'Beskrivning',
defaultValue: defaultValue:
'Berätta om era mål — vårt team kontaktar er och hjälper er hitta rätt lösning.', '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', name: 'submitText',
type: 'text', type: 'text',
localized: true,
label: 'Skicka-knapp text', label: 'Skicka-knapp text',
defaultValue: 'Skicka förfrågan', defaultValue: 'Skicka förfrågan',
}, },
@ -45,7 +48,7 @@ export const FDContactFormBlock: Block = {
type: 'row', type: 'row',
fields: [ fields: [
{ {
name: 'background', name: 'sectionBackground',
type: 'select', type: 'select',
label: 'Bakgrund', label: 'Bakgrund',
defaultValue: 'white', defaultValue: 'white',
@ -86,6 +89,7 @@ export const FDContactFormBlock: Block = {
{ {
name: 'privacyText', name: 'privacyText',
type: 'textarea', type: 'textarea',
localized: true,
label: 'Integritetstext', label: 'Integritetstext',
defaultValue: 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.', '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', name: 'privacyLinkText',
type: 'text', type: 'text',
localized: true,
label: 'Integritetslänk text', label: 'Integritetslänk text',
defaultValue: 'integritetspolicy', defaultValue: 'integritetspolicy',
admin: { width: '50%' }, admin: { width: '50%' },
@ -103,6 +108,7 @@ export const FDContactFormBlock: Block = {
{ {
name: 'privacyLinkUrl', name: 'privacyLinkUrl',
type: 'text', type: 'text',
localized: true,
label: 'Integritetslänk URL', label: 'Integritetslänk URL',
defaultValue: '/integritetspolicy', defaultValue: '/integritetspolicy',
admin: { width: '50%' }, admin: { width: '50%' },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ export const FDHeroBlock: Block = {
{ {
name: 'heading', name: 'heading',
type: 'text', type: 'text',
localized: true,
required: true, required: true,
label: 'Rubrik', label: 'Rubrik',
defaultValue: 'Sveriges bästa IT-ekosystem för företag', defaultValue: 'Sveriges bästa IT-ekosystem för företag',
@ -18,12 +19,14 @@ export const FDHeroBlock: Block = {
{ {
name: 'subheading', name: 'subheading',
type: 'text', type: 'text',
localized: true,
label: 'Underrubrik', label: 'Underrubrik',
defaultValue: 'Fiber, Backup, Colocation och Cloud', defaultValue: 'Fiber, Backup, Colocation och Cloud',
}, },
{ {
name: 'body', name: 'body',
type: 'textarea', type: 'textarea',
localized: true,
label: 'Brödtext', label: 'Brödtext',
defaultValue: 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.', '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', name: 'ctaText',
type: 'text', type: 'text',
localized: true,
label: 'CTA-knapp text', label: 'CTA-knapp text',
defaultValue: 'Kom igång', defaultValue: 'Kom igång',
}, },
{ {
name: 'ctaLink', name: 'ctaLink',
type: 'text', type: 'text',
localized: true,
label: 'CTA-knapp länk', label: 'CTA-knapp länk',
defaultValue: '/kontakt', defaultValue: '/kontakt',
}, },
{ {
name: 'secondaryCtaText', name: 'secondaryCtaText',
type: 'text', type: 'text',
localized: true,
label: 'Sekundär CTA text', label: 'Sekundär CTA text',
defaultValue: 'Kontakta oss', defaultValue: 'Kontakta oss',
}, },
{ {
name: 'secondaryCtaLink', name: 'secondaryCtaLink',
type: 'text', type: 'text',
localized: true,
label: 'Sekundär CTA länk', label: 'Sekundär CTA länk',
defaultValue: '/kontakt', defaultValue: '/kontakt',
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,11 +12,13 @@ export const FDVideoBlock: Block = {
{ {
name: 'heading', name: 'heading',
type: 'text', type: 'text',
localized: true,
label: 'Rubrik (valfri)', label: 'Rubrik (valfri)',
}, },
{ {
name: 'description', name: 'description',
type: 'textarea', type: 'textarea',
localized: true,
label: 'Beskrivning (valfri)', label: 'Beskrivning (valfri)',
}, },
// --- Video source --- // --- 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 = { export const FDVpsCalculatorBlock: Block = {
slug: 'fdVpsCalculator', slug: 'fdVpsCalculator',
interfaceName: 'FDVpsCalculatorBlock', interfaceName: 'FDVpsCalculatorBlock',
labels: { singular: 'VPS Kalkylator', plural: 'VPS Kalkylatorer' }, labels: { singular: 'FD VPS-kalkylator', plural: 'FD VPS-kalkylatorer' },
fields: [ fields: [
// ─── Presentation ────────────────────────────────────────────────────── // ─── Presentation ──────────────────────────────────────────────────────
{ name: 'heading', type: 'text', label: 'Rubrik', defaultValue: 'Virtuell server — kalkylator' }, { name: 'heading', type: 'text', label: 'Rubrik', defaultValue: 'Virtuell server — kalkylator' },

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import { Page, Post } from '@/payload-types'
import { getServerSideURL } from '@/utilities/getURL' import { getServerSideURL } from '@/utilities/getURL'
const generateTitle: GenerateTitle<Post | Page> = ({ doc }) => { 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 }) => { const generateURL: GenerateURL<Post | Page> = ({ doc }) => {
@ -49,6 +49,12 @@ export const plugins: Plugin[] = [
seoPlugin({ seoPlugin({
generateTitle, generateTitle,
generateURL, generateURL,
fields: ({ defaultFields }) => {
return defaultFields.map((field) => ({
...field,
localized: true,
}))
},
}), }),
formBuilderPlugin({ formBuilderPlugin({
fields: { fields: {