feat: schema freeze — standardize field names, richText fields, localization, admin labels
This commit is contained in:
parent
5cf9186ee2
commit
5ea9588223
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 })) ?? []
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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',
|
||||
},
|
||||
|
||||
@ -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%' },
|
||||
|
||||
@ -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: '#',
|
||||
},
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
|
||||
@ -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',
|
||||
},
|
||||
{
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
},
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
},
|
||||
{
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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)',
|
||||
|
||||
@ -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',
|
||||
},
|
||||
|
||||
@ -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',
|
||||
},
|
||||
{
|
||||
|
||||
@ -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',
|
||||
},
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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',
|
||||
},
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 ---
|
||||
|
||||
@ -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 på 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>
|
||||
)
|
||||
}
|
||||
@ -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' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -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' },
|
||||
|
||||
@ -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)',
|
||||
},
|
||||
{
|
||||
|
||||
@ -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 på 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;
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user