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-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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
{/* 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 */}
|
<div className="relative max-w-[1200px] mx-auto px-6 md:px-8 pt-16 pb-12 md:pt-24 md:pb-16">
|
||||||
<PayloadRedirects disableNotFound url={url} />
|
{/* 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} />
|
{/* Meta row */}
|
||||||
|
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 font-joey text-sm text-white/70">
|
||||||
<div className="flex flex-col items-center gap-4 pt-8">
|
{post.publishedAt && (
|
||||||
<div className="container">
|
<span>{formatDate(post.publishedAt)}</span>
|
||||||
<RichText className="max-w-[48rem] mx-auto" data={post.content} enableGutter={false} />
|
)}
|
||||||
{post.relatedPosts && post.relatedPosts.length > 0 && (
|
{authors.length > 0 && (
|
||||||
<RelatedPosts
|
<span>
|
||||||
className="mt-12 max-w-[52rem] lg:grid lg:grid-cols-subgrid col-start-1 col-span-3 grid-rows-[2fr]"
|
Av{' '}
|
||||||
docs={post.relatedPosts.filter((post) => typeof post === 'object')}
|
{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 })) ?? []
|
||||||
})
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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%' },
|
||||||
|
|||||||
@ -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: '#',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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)',
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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 ---
|
||||||
|
|||||||
@ -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 = {
|
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' },
|
||||||
|
|||||||
@ -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)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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 på desktop
|
* Visas till höger om formuläret på 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;
|
||||||
|
|||||||
@ -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 }) => {
|
||||||
@ -47,9 +47,15 @@ export const plugins: Plugin[] = [
|
|||||||
docs.reduce((url, doc) => (doc?.slug ? `${url}/${doc.slug}` : url), ''),
|
docs.reduce((url, doc) => (doc?.slug ? `${url}/${doc.slug}` : url), ''),
|
||||||
}),
|
}),
|
||||||
seoPlugin({
|
seoPlugin({
|
||||||
generateTitle,
|
generateTitle,
|
||||||
generateURL,
|
generateURL,
|
||||||
}),
|
fields: ({ defaultFields }) => {
|
||||||
|
return defaultFields.map((field) => ({
|
||||||
|
...field,
|
||||||
|
localized: true,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
}),
|
||||||
formBuilderPlugin({
|
formBuilderPlugin({
|
||||||
fields: {
|
fields: {
|
||||||
payment: false,
|
payment: false,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user