Design system polish: borders, radius, typography, calculator, overlay, announcement bar + new services calculator

This commit is contained in:
Jeffrey 2026-02-21 12:33:22 +01:00
parent 606a4c27b4
commit f838dc1c6e
68 changed files with 1262 additions and 303447 deletions

3
.gitignore vendored
View File

@ -17,3 +17,6 @@ src/**/*.bak.*
next-env.d.ts
tsconfig.tsbuildinfo
media/
src/migrations/*.ts
src/migrations/*.json
!src/migrations/index.ts

View File

@ -1,54 +0,0 @@
#!/bin/bash
# Run from your project root (wwwfiberdirekt)
B="src/blocks"
add_thumbnail() {
local file="$1"
local image="$2"
local alt="$3"
if [ ! -f "$file" ]; then
echo "⚠ MISSING: $file"
return
fi
# Skip if already added
if grep -q "imageURL" "$file"; then
echo "⏭ SKIPPED (already has imageURL): $file"
return
fi
sed -i '' "s|slug: '\([^']*\)',|slug: '\1',\n imageURL: '/block-thumbnails/${image}',\n imageAltText: '${alt}',|" "$file"
echo "$file"
}
# Blocks with thumbnails
add_thumbnail "$B/FDHeroBlock/config.ts" "fd-hero.png" "FD Hero"
add_thumbnail "$B/FDAlternateHeroBlock/config.ts" "fd-header-text-image-alt.png" "FD Alternativ Hero"
add_thumbnail "$B/FDHeaderTextImageBlock/config.ts" "fd-header-text-image.png" "FD Rubrik med bild"
add_thumbnail "$B/FDCardGridBlock/config.ts" "fd-card-grid.png" "FD Kortrutnät"
add_thumbnail "$B/FDWideCardBlock/config.ts" "fd-wide-card.png" "FD Bredt kort"
add_thumbnail "$B/FDPricingCardBlock/config.ts" "fd-pricing-card.png" "FD Priskort"
add_thumbnail "$B/FDUspTableBlock/config.ts" "fd-usp-table.png" "FD USP-tabell"
add_thumbnail "$B/FDUspChecklistBlock/config.ts" "fd-usp-checklist.png" "FD USP-checklista"
add_thumbnail "$B/FDServicesGridBlock/config.ts" "fd-services-grid.png" "FD Tjänsterrutnät"
add_thumbnail "$B/FDIconBarBlock/config.ts" "fd-icon-bar.png" "FD Ikonrad"
add_thumbnail "$B/FDFeatureAnnouncementBlock/config.ts" "fd-feature-announcement.png" "FD Funktionsnyhet"
add_thumbnail "$B/FDTechPropertiesBlock/config.ts" "fd-tech-properties.png" "FD Tekniska Egenskaper"
add_thumbnail "$B/FDCtaSideImageBlock/config.ts" "fd-cta-side-image.png" "FD CTA med bild"
add_thumbnail "$B/FDFaqBlock/config.ts" "fd-faq.png" "FD FAQ"
add_thumbnail "$B/FDContactBlock/config.ts" "fd-contact.png" "FD Kontaktformulär"
add_thumbnail "$B/FDContactFormBlock/config.ts" "fd-contact.png" "FD Kontaktformulär"
add_thumbnail "$B/FDSpacerBlock/config.ts" "fd-spacer.png" "FD Mellanrum"
add_thumbnail "$B/FDLocationsGridBlock/config.ts" "fd-places-grid.png" "FD Platser Grid"
echo ""
echo "Blocks without thumbnails yet (add images later):"
echo " FDCtaBannerBlock, FDCodeEmbedBlock, FDDataTableBlock"
echo " FDNewsletterBlock, FDPartnersLogosBlock, FDServiceChooserBlock"
echo " FDStatisticsBlock, FDTagsBlock, FDTeamBlock, FDTestimonialBlock"
echo " FDTextBlock, FDVideoBlock, FDVpsCalculatorBlock"
echo ""
echo "Done. Verify: grep -r 'imageURL' src/blocks/ | wc -l"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -104,7 +104,8 @@ export async function Footer() {
<nav className="flex flex-col gap-2.5">
{column.links?.map(({ link }, linkIndex) => (
<CMSLink
className="font-joey text-white/70 hover:text-white text-sm md:text-base transition-colors no-underline"
/* Quick win: was text-sm md:text-base — now text-sm consistently */
className="font-joey text-white/70 hover:text-white text-sm transition-colors no-underline"
key={linkIndex}
{...link}
/>
@ -147,11 +148,13 @@ export async function Footer() {
})}
</div>
)}
<p className="font-joey text-white/60 text-base whitespace-nowrap">{bottomLeft}</p>
{/* Quick win: was text-base — now text-sm to match nav links below */}
<p className="font-joey text-white/60 text-sm whitespace-nowrap">{bottomLeft}</p>
{navItems.length > 0 && (
<nav className="flex items-center gap-4 flex-wrap">
{navItems.map(({ link }, i) => (
<CMSLink
/* Consistent text-sm across bottom row */
className="font-joey text-white/60 hover:text-white text-sm transition-colors no-underline underline-offset-2 hover:underline"
key={i}
{...link}

View File

@ -1,5 +1,5 @@
'use client'
import React, { useState, useEffect, useRef } from 'react'
import React, { useState, useEffect, useRef, useCallback } from 'react'
import type { Header as HeaderType, Page } from '@/payload-types'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
@ -56,6 +56,70 @@ function groupChildren(children: NavChild[]): { heading: string | null; links: N
return result
}
/* ── Priority #4: Focus trap hook for mobile menu ──────────────────── */
function useFocusTrap(containerRef: React.RefObject<HTMLElement | null>, active: boolean) {
useEffect(() => {
if (!active || !containerRef.current) return
const container = containerRef.current
const focusableSelector =
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
const getFocusableElements = () =>
Array.from(container.querySelectorAll<HTMLElement>(focusableSelector))
// Focus the first focusable element when trap activates
const elements = getFocusableElements()
if (elements.length > 0) {
// Small delay to ensure the menu transition has started
setTimeout(() => elements[0]?.focus(), 100)
}
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return
const focusable = getFocusableElements()
if (focusable.length === 0) return
const first = focusable[0]
const last = focusable[focusable.length - 1]
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault()
last.focus()
}
} else {
if (document.activeElement === last) {
e.preventDefault()
first.focus()
}
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [active, containerRef])
}
/* ── Priority #10: Check if a nav item matches current path ────────── */
function isActiveSection(item: NavItem, pathname: string | null): boolean {
if (!pathname || !item.children?.length) {
// For top-level links without children, check direct href match
const href = resolveHref(item)
if (href === '/') return pathname === '/'
return pathname?.startsWith(href) || false
}
// For dropdown parents, check if any child href matches
return item.children.some((child) => {
const childHref = resolveHref(child)
if (childHref === '/' || childHref === '#') return false
return pathname?.startsWith(childHref) || false
})
}
const MegaMenuPanel: React.FC<{
item: NavItem
headerRef: React.RefObject<HTMLElement | null>
@ -98,7 +162,7 @@ const MegaMenuPanel: React.FC<{
style={{ top }}
>
<div className="max-w-[1200px] mx-auto px-6 md:px-8 py-10 md:py-14">
<div className="grid grid-cols-[200px_1fr] gap-12 md:gap-20">
<div className="grid grid-cols-1 md:grid-cols-[200px_1fr] gap-8 md:gap-12 lg:gap-20">
{/* Left: category title */}
<div>
@ -156,6 +220,10 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
const navRef = useRef<HTMLDivElement>(null)
const headerRef = useRef<HTMLElement | null>(null)
const megaMenuRef = useRef<HTMLDivElement | null>(null)
const mobileMenuRef = useRef<HTMLDivElement | null>(null)
// Priority #4: Focus trap
useFocusTrap(mobileMenuRef, mobileOpen)
useEffect(() => {
if (navRef.current) {
@ -213,13 +281,19 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
const hasChildren = item.children && item.children.length > 0
const isOpen = openDropdown === item.label
const isMega = item.megaMenu && hasChildren
// Priority #10: Active section detection
const isActive = isActiveSection(item, pathname)
return hasChildren ? (
<div key={i} className="relative">
<button
onClick={() => setOpenDropdown(isOpen ? null : item.label ?? null)}
className={`flex items-center gap-1 font-joey-bold transition-colors text-base ${
isOpen ? 'text-fd-yellow' : 'text-fd-navy dark:text-white hover:text-fd-yellow dark:hover:text-fd-yellow'
isOpen
? 'text-fd-yellow'
: isActive
? 'text-fd-yellow'
: 'text-fd-navy dark:text-white hover:text-fd-yellow dark:hover:text-fd-yellow'
}`}
aria-haspopup="true"
aria-expanded={isOpen}
@ -228,6 +302,10 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
<ChevronDownIcon
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
/>
{/* Active indicator dot */}
{isActive && !isOpen && (
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-fd-yellow" />
)}
</button>
{/* Small card dropdown */}
@ -256,9 +334,16 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
<Link
key={i}
href={resolveHref(item)}
className="font-joey-bold text-fd-navy dark:text-white hover:text-fd-yellow dark:hover:text-fd-yellow transition-colors text-base"
className={`relative font-joey-bold transition-colors text-base ${
isActive
? 'text-fd-yellow'
: 'text-fd-navy dark:text-white hover:text-fd-yellow dark:hover:text-fd-yellow'
}`}
>
{item.label}
{isActive && (
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-fd-yellow" />
)}
</Link>
)
})}
@ -285,8 +370,9 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
{mobileOpen ? <XIcon className="w-6 h-6" /> : <MenuIcon className="w-6 h-6" />}
</button>
{/* ── Mobile full-screen overlay ── */}
{/* ── Mobile full-screen overlay (Priority #4: has focus trap via ref) ── */}
<div
ref={mobileMenuRef}
className={`fixed inset-0 z-50 bg-fd-navy flex flex-col transition-transform duration-300 md:hidden ${
mobileOpen ? 'translate-x-0' : 'translate-x-full'
}`}

View File

@ -239,7 +239,7 @@ html:not([data-theme]) {
@font-face {
font-family: 'FS Joey';
src: url('/fonts/fs-joey-regular.otf') format('opentype');
src: url('/fonts/fs-joey-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
@ -247,7 +247,7 @@ html:not([data-theme]) {
@font-face {
font-family: 'FS Joey';
src: url('/fonts/fs-joey-italic.otf') format('opentype');
src: url('/fonts/fs-joey-italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
@ -255,7 +255,7 @@ html:not([data-theme]) {
@font-face {
font-family: 'FS Joey Medium';
src: url('/fonts/fs-joey-medium.otf') format('opentype');
src: url('/fonts/fs-joey-medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@ -263,7 +263,7 @@ html:not([data-theme]) {
@font-face {
font-family: 'FS Joey Bold';
src: url('/fonts/fs-joey-bold.otf') format('opentype');
src: url('/fonts/fs-joey-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@ -271,7 +271,7 @@ html:not([data-theme]) {
@font-face {
font-family: 'FS Joey Heavy';
src: url('/fonts/fs-joey-heavy.otf') format('opentype');
src: url('/fonts/fs-joey-heavy.woff2') format('woff2');
font-weight: 900;
font-style: normal;
font-display: swap;
@ -592,6 +592,37 @@ html:not([data-theme]) {
box-shadow: 0 4px 12px -2px rgba(103, 239, 139, 0.25);
}
/* ---- Focus-visible states (WCAG 2.4.7) ---- */
.fd-btn-primary:focus-visible,
.fd-btn-primary-dark:focus-visible {
outline: 3px solid var(--color-fd-navy);
outline-offset: 3px;
box-shadow: 0 0 0 6px rgba(254, 204, 2, 0.3);
}
.fd-btn-secondary:focus-visible {
outline: 3px solid var(--color-fd-navy);
outline-offset: 3px;
box-shadow: 0 0 0 6px rgba(14, 35, 56, 0.15);
}
.fd-btn-secondary-dark:focus-visible {
outline: 3px solid #FFFFFF;
outline-offset: 3px;
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.2);
}
.fd-btn-navy:focus-visible {
outline: 3px solid var(--color-fd-yellow);
outline-offset: 3px;
box-shadow: 0 0 0 6px rgba(14, 35, 56, 0.2);
}
.fd-btn-mint:focus-visible {
outline: 3px solid var(--color-fd-navy);
outline-offset: 3px;
box-shadow: 0 0 0 6px rgba(103, 239, 139, 0.25);
}
/* ---- Standard Section Wrapper ----
Usage: <section class="fd-section">
@ -657,7 +688,7 @@ html:not([data-theme]) {
============================================ */
@media (max-width: 47.9375rem) {
/* Full-width buttons on mobile for clear tap targets */
/* Full-width CTA buttons on mobile for clear tap targets */
.fd-btn-primary,
.fd-btn-primary-dark,
.fd-btn-secondary,
@ -668,15 +699,12 @@ html:not([data-theme]) {
justify-content: center;
padding-top: 0.875rem;
padding-bottom: 0.875rem;
}
/* Ensure minimum touch target */
a, button {
min-height: 44px;
min-height: 48px;
}
}
/* ============================================
FIBER DIREKT FRONTEND DARK MODE
============================================

View File

@ -27,7 +27,8 @@ const cardStyleMap: Record<
border: '',
},
green: {
bg: 'bg-[#4ADE80]',
/* Priority #9: Was bg-[#4ADE80] (Tailwind default) — now uses brand mint */
bg: 'bg-fd-mint',
headingText: 'text-fd-navy',
bodyText: 'text-fd-navy',
linkText: 'text-fd-navy hover:text-fd-navy/70',
@ -38,7 +39,7 @@ const cardStyleMap: Record<
headingText: 'text-fd-navy dark:text-fd-yellow',
bodyText: 'text-fd-navy dark:text-white',
linkText: 'text-fd-navy/70 hover:text-fd-navy dark:text-fd-yellow/80 dark:hover:text-fd-yellow',
border: 'border-6 border-gray-200 dark:border-white/10 shadow-sm',
border: 'border-5 border-gray-200 dark:border-white/10 shadow-sm',
},
}
@ -62,6 +63,9 @@ const styleClassMap: Record<string, string> = {
boldItalic: 'font-joey-bold italic',
}
/* Priority #5: Responsive radius constant */
const cardRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
export const FDCardGridBlockComponent: React.FC<FDCardGridBlockProps> = ({
layout = '1-1-1',
cardStyle = 'outlined',
@ -84,7 +88,7 @@ export const FDCardGridBlockComponent: React.FC<FDCardGridBlockProps> = ({
if (mode === 'centeredHeading') {
cardContent = (
<div
className={`${style.bg} ${style.border} rounded-[70px] px-8 md:px-14 py-14 md:py-20 flex items-center justify-center min-h-[280px] md:min-h-[360px]`}
className={`${style.bg} ${style.border} ${cardRadius} px-8 md:px-14 py-14 md:py-20 flex items-center justify-center min-h-[280px] md:min-h-[360px]`}
>
<span
className={`font-joey-heavy text-fd-display leading-tight text-center ${style.headingText}`}
@ -96,7 +100,7 @@ export const FDCardGridBlockComponent: React.FC<FDCardGridBlockProps> = ({
} else if (mode === 'centeredBody') {
cardContent = (
<div
className={`${style.bg} ${style.border} rounded-[70px] px-8 md:px-14 py-14 md:py-20 flex items-center justify-center min-h-[280px] md:min-h-[360px]`}
className={`${style.bg} ${style.border} ${cardRadius} px-8 md:px-14 py-14 md:py-20 flex items-center justify-center min-h-[280px] md:min-h-[360px]`}
>
<p
className={`font-joey text-fd-body-lg text-center ${style.bodyText}`}
@ -106,10 +110,9 @@ export const FDCardGridBlockComponent: React.FC<FDCardGridBlockProps> = ({
</div>
)
} else {
// FIX: px-6 on mobile → px-10 on md+ to prevent 40px padding squashing 375px cards
cardContent = (
<div
className={`${style.bg} ${style.border} rounded-[70px] px-6 md:px-10 lg:px-14 py-8 md:py-12 lg:py-14 flex flex-col gap-1 h-full`}
className={`${style.bg} ${style.border} ${cardRadius} px-6 md:px-10 lg:px-14 py-8 md:py-12 lg:py-14 flex flex-col gap-1 h-full`}
>
{card.heading && (
<h3
@ -150,7 +153,8 @@ export const FDCardGridBlockComponent: React.FC<FDCardGridBlockProps> = ({
<a
key={index}
href={card.cardLink}
className="block hover:opacity-90 transition-opacity"
className="block hover:shadow-lg hover:-translate-y-1 transition-all duration-200"
aria-label={card.heading || undefined}
>
{cardContent}
</a>

View File

@ -19,6 +19,9 @@ const bgClasses: Record<string, string> = {
transparent: 'bg-transparent',
}
/* Priority #5: Responsive radius for embed card wrappers */
const cardRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
export const FDCodeEmbedBlockComponent: React.FC<FDCodeEmbedBlockProps> = ({
heading,
description,
@ -57,9 +60,9 @@ export const FDCodeEmbedBlockComponent: React.FC<FDCodeEmbedBlockProps> = ({
const embedWrapperClass =
embedBackground === 'card'
? 'bg-white dark:bg-white/10 rounded-[70px] shadow-lg p-6 md:p-10 overflow-hidden'
? `bg-white dark:bg-white/10 ${cardRadius} shadow-lg p-6 md:p-10 overflow-hidden`
: embedBackground === 'navy-card'
? 'bg-fd-navy rounded-[70px] shadow-lg p-6 md:p-10 overflow-hidden'
? `bg-fd-navy ${cardRadius} shadow-lg p-6 md:p-10 overflow-hidden`
: ''
useEffect(() => {
@ -98,7 +101,7 @@ export const FDCodeEmbedBlockComponent: React.FC<FDCodeEmbedBlockProps> = ({
{(heading || description) && (
<div className="mb-8 md:mb-12 text-center">
{heading && (
<h2 className={`font-joey-medium text-fd-h1 mb-4 ${headingColor}`}>
<h2 className={`font-joey-heavy text-fd-h1 mb-4 ${headingColor}`}>
{heading}
</h2>
)}

View File

@ -2,6 +2,9 @@ import React from 'react'
import type { FDContactBlock as FDContactBlockProps, Media } from '@/payload-types'
import { FDImage } from '@/components/FDImage'
/* Smaller radius for compact contact method images — the one exception per Jeffrey */
const imageRadius = 'rounded-[16px] md:rounded-[24px] lg:rounded-[30px]'
export const FDContactBlockComponent: React.FC<FDContactBlockProps> = ({
heading,
contactMethods,
@ -9,19 +12,17 @@ export const FDContactBlockComponent: React.FC<FDContactBlockProps> = ({
return (
<section className="relative w-full bg-fd-navy py-16 md:py-20 lg:pt-[100px] lg:pb-[120px]">
<div className="max-w-[1200px] mx-auto px-6 md:px-8 flex flex-col items-center gap-8 lg:gap-10">
{/* FIX: removed conflicting text-fd-navy class, kept text-fd-yellow */}
<h2 className="w-full font-joey-heavy text-fd-h1 text-fd-yellow text-center">
{heading}
</h2>
{/* FIX: flex-col on mobile → sm:flex-row so cards don't squash at 375px */}
<div className="flex flex-col sm:flex-row items-stretch gap-4 md:gap-7 w-full max-w-[656px]">
{contactMethods?.map((method, index) => {
const media = method.icon as Media
const card = (
<div className="flex-1 flex flex-col items-center gap-3 md:gap-5 cursor-pointer transition-transform hover:scale-105">
{media?.url && (
<div className="relative w-full h-[140px] sm:h-[120px] md:h-[160px] lg:h-[200px] overflow-hidden rounded-[70px]">
<div className={`relative w-full h-[140px] sm:h-[120px] md:h-[160px] lg:h-[200px] overflow-hidden ${imageRadius}`}>
<FDImage
media={media}
size="medium"

View File

@ -2,7 +2,7 @@ import type { Block } from 'payload'
export const FDContactBlock: Block = {
slug: 'fdContact',
imageURL: '/block-thumbnails/fd-contact.png',
imageURL: '/block-thumbnails/fd-contact-info.png',
imageAltText: 'FD Kontaktformulär',
interfaceName: 'FDContactBlock',
labels: {

View File

@ -17,9 +17,10 @@ const sectionBgMap: Record<string, string> = {
navyGradient: 'bg-gradient-to-br from-fd-navy via-[#153350] to-fd-navy',
}
// Navy/navyGradient are always dark. White/gray adapt via OS dark: classes.
const isExplicitDark = (bg: string) => bg === 'navy' || bg === 'navyGradient'
const cardRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
@ -92,8 +93,6 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
}
/* ---- Input styles ---- */
// Light sections: light inputs + dark: variants for OS dark mode
// Dark sections: always dark inputs
const inputBase = 'w-full rounded-xl px-4 py-3 font-joey text-base outline-none transition-all'
@ -286,7 +285,8 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className={`font-joey-heavy text-3xl md:text-4xl mb-4 ${headingColor}`}>Tack!</h2>
{/* Priority #6: Use fluid token instead of hardcoded sizes */}
<h2 className={`font-joey-heavy text-fd-h1 mb-4 ${headingColor}`}>Tack!</h2>
<p className={`font-joey text-lg ${bodyColor}`}>
Vi har tagit emot din förfrågan och återkommer snart vi kan.
</p>
@ -312,7 +312,8 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
const formContent = (
<>
{heading && (
<h2 className={`font-joey-heavy text-3xl md:text-4xl lg:text-5xl leading-tight mb-3 ${headingColor}`}>
/* Priority #6: Use fluid token text-fd-h1 instead of hardcoded text-3xl/4xl/5xl */
<h2 className={`font-joey-heavy text-fd-h1 leading-tight mb-3 ${headingColor}`}>
{heading}
</h2>
)}
@ -323,7 +324,6 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
{renderFields()}
{/* Submit — uses FDButton, onDark when section is navy or OS is in dark mode */}
<div className="mt-2">
<FDButton
as="button"
@ -367,7 +367,7 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
<section className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}>
<div className="max-w-[1200px] mx-auto px-6 md:px-8">
<div
className={`max-w-[720px] mx-auto p-8 md:p-12 rounded-[40px] md:rounded-[70px] ${
className={`max-w-[720px] mx-auto p-8 md:p-12 ${cardRadius} ${
dark
? 'bg-white/5 border border-white/10 backdrop-blur-sm'
: 'bg-white shadow-lg border border-gray-100 dark:bg-white/5 dark:border-white/10 dark:backdrop-blur-sm'
@ -392,7 +392,7 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
<FDImage
media={media!}
size="medium"
className="w-[380px] h-auto max-h-[560px] object-cover rounded-[70px]"
className="w-[380px] h-auto max-h-[560px] object-cover rounded-[32px] md:rounded-[50px] lg:rounded-[70px]"
sizes="380px"
fallbackAlt={heading || ''}
/>

View File

@ -4,17 +4,34 @@ import type { Media } from '@/payload-types'
import { FDImage } from '@/components/FDImage'
import { FDButton } from '@/components/FDButton'
/* Priority #5: Responsive radius */
const imageRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
/* Color overlay — same map as FDHeaderTextImageBlock */
const overlayMap: Record<string, string> = {
none: '',
navyLight: 'bg-fd-navy/20',
navyMedium: 'bg-fd-navy/40',
yellowLight: 'bg-fd-yellow/20',
yellowMedium:'bg-fd-yellow/40',
sepia: 'bg-[#8B7D3C]/30',
blackLight: 'bg-black/20',
blackMedium: 'bg-black/40',
}
export const FDCtaSideImageBlockComponent: React.FC<FDCtaSideImageBlockProps> = ({
heading,
body,
ctaText,
ctaLink = '#',
image,
imageOverlay = 'none',
imagePosition = 'right',
theme = 'dark',
}) => {
const isDark = theme === 'dark'
const hasImage = !!image
const overlay = overlayMap[(imageOverlay as string) || 'none'] || ''
// Light theme adapts to OS dark preference → becomes navy bg
const sectionBg = isDark
@ -41,7 +58,6 @@ export const FDCtaSideImageBlockComponent: React.FC<FDCtaSideImageBlockProps> =
</div>
{ctaText && (
// onDark — either explicit dark theme, or dark: mode makes it navy anyway
<FDButton href={ctaLink || '#'} variant="primary" onDark={isDark}>
{ctaText}
</FDButton>
@ -50,14 +66,15 @@ export const FDCtaSideImageBlockComponent: React.FC<FDCtaSideImageBlockProps> =
)
const imageContent = hasImage ? (
<div className="w-full lg:w-[575px] lg:h-[479px] flex-shrink-0">
<div className={`relative w-full lg:w-[575px] lg:h-[479px] flex-shrink-0 overflow-hidden ${imageRadius}`}>
<FDImage
media={image as Media}
size="large"
fallbackAlt={heading}
className="w-full h-full object-cover rounded-[70px]"
className="w-full h-full object-cover"
sizes="(max-width: 1024px) 100vw, 575px"
/>
{overlay && <div className={`absolute inset-0 ${overlay}`} />}
</div>
) : null

View File

@ -44,6 +44,25 @@ export const FDCtaSideImageBlock: Block = {
relationTo: 'media',
label: 'Bild (valfri)',
},
{
name: 'imageOverlay',
type: 'select',
label: 'Bildöverlagring',
defaultValue: 'none',
options: [
{ label: 'Ingen', value: 'none' },
{ label: 'Navy (lätt)', value: 'navyLight' },
{ label: 'Navy (medium)', value: 'navyMedium' },
{ label: 'Gul (lätt)', value: 'yellowLight' },
{ label: 'Gul (medium)', value: 'yellowMedium' },
{ label: 'Sepia', value: 'sepia' },
{ label: 'Svart (lätt)', value: 'blackLight' },
{ label: 'Svart (medium)', value: 'blackMedium' },
],
admin: {
condition: (_, siblingData) => Boolean(siblingData?.image),
},
},
{
name: 'imagePosition',
type: 'select',

View File

@ -1,5 +1,5 @@
'use client'
import React, { useState } from 'react'
import React, { useState, useId } from 'react'
import type { FDFaqBlock as FDFaqBlockProps } from '@/payload-types'
import RichText from '@/components/RichText'
@ -9,6 +9,8 @@ export const FDFaqBlockComponent: React.FC<FDFaqBlockProps> = ({
theme = 'gray',
}) => {
const [openIndex, setOpenIndex] = useState<number | null>(null)
/* Generate a stable unique prefix for this block instance */
const blockId = useId()
// dark theme = always navy. gray/white adapt via dark: OS preference
const bgClass =
@ -41,45 +43,57 @@ export const FDFaqBlockComponent: React.FC<FDFaqBlockProps> = ({
{heading}
</h2>
<div className="w-full max-w-[1162px]">
{items?.map((item, index) => (
<div key={index} className={`border-b ${borderColor}`}>
<button
className={`flex w-full items-center gap-3 md:gap-4 py-4 md:py-5 text-left transition-colors ${textColor}`}
onClick={() => setOpenIndex(openIndex === index ? null : index)}
aria-expanded={openIndex === index}
>
<svg
className={`w-4 h-4 md:w-5 md:h-5 flex-shrink-0 transition-transform duration-200 ${
openIndex === index ? 'rotate-45' : ''
}`}
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth="2"
{items?.map((item, index) => {
const isOpen = openIndex === index
/* Accessibility: unique IDs for aria-controls ↔ id linkage */
const triggerId = `${blockId}-faq-trigger-${index}`
const panelId = `${blockId}-faq-panel-${index}`
return (
<div key={index} className={`border-b ${borderColor}`}>
<button
id={triggerId}
className={`flex w-full items-center gap-3 md:gap-4 py-4 md:py-5 text-left transition-colors ${textColor}`}
onClick={() => setOpenIndex(isOpen ? null : index)}
aria-expanded={isOpen}
aria-controls={panelId}
>
<line x1="10" y1="2" x2="10" y2="18" />
<line x1="2" y1="10" x2="18" y2="10" />
</svg>
<span className="font-joey text-fd-h3">
{item.question}
</span>
</button>
<div
className={`grid transition-all duration-200 ease-out ${
openIndex === index
? 'grid-rows-[1fr] opacity-100 pb-5'
: 'grid-rows-[0fr] opacity-0'
}`}
>
<div className="overflow-hidden">
{/* No pl- here — answer aligns flush with the left edge of the question row */}
<div className={`font-joey text-fd-body fd-prose ${proseColor}`}>
<RichText data={(item.answer) as any} />
<svg
className={`w-4 h-4 md:w-5 md:h-5 flex-shrink-0 transition-transform duration-200 ${
isOpen ? 'rotate-45' : ''
}`}
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true"
>
<line x1="10" y1="2" x2="10" y2="18" />
<line x1="2" y1="10" x2="18" y2="10" />
</svg>
<span className="font-joey text-fd-h3">
{item.question}
</span>
</button>
<div
id={panelId}
role="region"
aria-labelledby={triggerId}
className={`grid transition-all duration-200 ease-out ${
isOpen
? 'grid-rows-[1fr] opacity-100 pb-5'
: 'grid-rows-[0fr] opacity-0'
}`}
>
<div className="overflow-hidden">
<div className={`font-joey text-fd-body fd-prose ${proseColor}`}>
<RichText data={(item.answer) as any} />
</div>
</div>
</div>
</div>
</div>
))}
)
})}
</div>
</div>
</section>

View File

@ -31,10 +31,11 @@ const overlayMap: Record<string, string> = {
blackMedium: 'bg-black/40',
}
/* Updated: responsive radius matching the standard system */
const roundedMap: Record<string, string> = {
none: '',
medium: 'rounded-[24px]',
large: 'rounded-[40px]',
medium: 'rounded-[20px] md:rounded-[32px] lg:rounded-[40px]',
large: 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]',
}
export const FDHeaderTextImageBlockComponent: React.FC<FDHeaderTextImageBlockProps> = ({

View File

@ -52,8 +52,8 @@ export const FDHeaderTextImageBlock: Block = {
defaultValue: 'large',
options: [
{ label: 'Ingen', value: 'none' },
{ label: 'Medium (24px)', value: 'medium' },
{ label: 'Stor (40px)', value: 'large' },
{ label: 'Medium (40px)', value: 'medium' },
{ label: 'Stor (70px)', value: 'large' },
],
},
{

View File

@ -1,6 +1,7 @@
import React from 'react'
import type { FDHeroBlock as FDHeroBlockProps, Media } from '@/payload-types'
import { FDButton } from '@/components/FDButton'
import { FDImage } from '@/components/FDImage'
export const FDHeroBlockComponent: React.FC<FDHeroBlockProps> = ({
heading,
@ -16,8 +17,7 @@ export const FDHeroBlockComponent: React.FC<FDHeroBlockProps> = ({
theme = 'light',
}) => {
const media = backgroundImage as Media | undefined
const bgImageUrl = media?.url || ''
const hasBgImage = Boolean(bgImageUrl)
const hasBgImage = Boolean(media?.url)
const isDark = hasBgImage || theme === 'dark'
let headingColor: string
@ -44,23 +44,33 @@ export const FDHeroBlockComponent: React.FC<FDHeroBlockProps> = ({
hasBgImage ? '' : isDark ? 'bg-fd-navy' : 'bg-white dark:bg-fd-navy'
} overflow-hidden`}
>
{hasBgImage && (
{/* Priority #2: Use FDImage instead of raw <img> for Next.js optimization */}
{hasBgImage && media && (
<>
<img src={bgImageUrl} alt="" className="absolute inset-0 w-full h-full object-cover" aria-hidden="true" />
<FDImage
media={media}
size="hero"
fill
priority
className="absolute inset-0 w-full h-full object-cover"
sizes="100vw"
fallbackAlt=""
/>
<div className={`absolute inset-0 ${overlayClass}`} aria-hidden="true" />
</>
)}
<div className="relative max-w-[1200px] mx-auto px-6 md:px-8 flex flex-col items-start gap-6 md:gap-8">
<h1 className={`w-full max-w-[884px] font-joey-heavy text-3xl md:text-5xl lg:text-[78px] leading-tight lg:leading-none ${headingColor}`}>
{/* Priority #6: Use fluid token text-fd-display instead of hardcoded breakpoints */}
<h1 className={`w-full max-w-[884px] font-joey-heavy text-fd-display leading-tight lg:leading-none ${headingColor}`}>
{heading}
</h1>
{subheading && (
<h2 className={`w-full max-w-[884px] font-joey-medium text-2xl md:text-4xl lg:text-[50px] leading-tight ${textBodyColor}`}>
<h2 className={`w-full max-w-[884px] font-joey-medium text-fd-h1 leading-tight ${textBodyColor}`}>
{subheading}
</h2>
)}
{body && (
<p className={`w-full max-w-[597px] font-joey text-lg md:text-xl lg:text-2xl lg:leading-snug ${textBodyColor}`}>
<p className={`w-full max-w-[597px] font-joey text-fd-body-lg lg:leading-snug ${textBodyColor}`}>
{body}
</p>
)}

View File

@ -38,7 +38,8 @@ export const FDLocationsGridBlockComponent: React.FC<Props> = ({
{(heading || description || ctaText) && (
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-6 mb-12 md:mb-16">
{heading && (
<h2 className={`font-joey-medium text-fd-h1 ${titleClass} lg:max-w-[380px]`}>
/* Priority #6: Was font-joey-medium — now font-joey-heavy for consistency with all section headings */
<h2 className={`font-joey-heavy text-fd-h1 ${titleClass} lg:max-w-[380px]`}>
{heading}
</h2>
)}
@ -59,7 +60,7 @@ export const FDLocationsGridBlockComponent: React.FC<Props> = ({
{(cards ?? []).map((card, i) => {
const media = card.image as Media | undefined
const isLink = Boolean(card.link)
const className = `group relative overflow-hidden rounded-[30px] md:rounded-[50px] aspect-[4/3] block ${isLink ? 'cursor-pointer' : ''}`
const className = `group relative overflow-hidden rounded-[32px] md:rounded-[50px] lg:rounded-[70px] aspect-[4/3] block ${isLink ? 'cursor-pointer' : ''}`
const inner = (
<>

View File

@ -11,6 +11,9 @@ const bgClasses: Record<string, string> = {
yellow: 'bg-fd-yellow',
}
/* Priority #5: Responsive card radius */
const cardRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
export const FDNewsletterBlockComponent: React.FC<FDNewsletterBlockProps> = ({
heading = 'Håll dig uppdaterad',
description,
@ -82,6 +85,14 @@ export const FDNewsletterBlockComponent: React.FC<FDNewsletterBlockProps> = ({
[email, name, company, consented, submitEndpoint, collectName, collectCompany],
)
/* Accessibility: explain why button is disabled */
const isDisabled = !email || !consented || status === 'loading'
const disabledReason = !email
? 'Fyll i din e-postadress'
: !consented
? 'Du behöver godkänna villkoren'
: undefined
if (status === 'success') {
return (
<section className={`relative w-full py-16 md:py-20 lg:py-[99px] ${bgClass} overflow-hidden`}>
@ -106,10 +117,11 @@ export const FDNewsletterBlockComponent: React.FC<FDNewsletterBlockProps> = ({
<section className={`relative w-full py-16 md:py-20 lg:py-[99px] ${bgClass} overflow-hidden`}>
<div className="relative max-w-[1200px] mx-auto px-6 md:px-8">
{isCard ? (
<div className="max-w-[700px] mx-auto bg-white/10 backdrop-blur-sm rounded-[70px] p-8 md:p-12 lg:p-16">
/* Priority #5: Responsive radius on card */
<div className={`max-w-[700px] mx-auto bg-white/10 backdrop-blur-sm ${cardRadius} p-8 md:p-12 lg:p-16`}>
<div className="text-center mb-8">
{heading && (
<h2 className={`font-joey-medium text-fd-h1 mb-4 ${headingColor}`}>
<h2 className={`font-joey-heavy text-fd-h1 mb-4 ${headingColor}`}>
{heading}
</h2>
)}
@ -128,13 +140,18 @@ export const FDNewsletterBlockComponent: React.FC<FDNewsletterBlockProps> = ({
<ConsentCheckbox consented={consented} setConsented={setConsented} consentText={consentText} privacyPolicyLink={privacyPolicyLink} isDark={isDark} bodyColor={bodyColor} />
<button
onClick={handleSubmit}
disabled={!email || !consented || status === 'loading'}
disabled={isDisabled}
aria-disabled={isDisabled}
aria-describedby={isDisabled ? 'newsletter-disabled-reason' : undefined}
className="fd-btn-primary w-full mt-4 disabled:opacity-50 disabled:cursor-not-allowed"
>
{status === 'loading' ? 'Skickar...' : buttonText}
</button>
{isDisabled && disabledReason && (
<p id="newsletter-disabled-reason" className="sr-only">{disabledReason}</p>
)}
{status === 'error' && (
<p className="text-red-400 text-fd-small mt-3 text-center font-joey">{errorMsg}</p>
<p className="text-red-400 text-fd-small mt-3 text-center font-joey" role="alert">{errorMsg}</p>
)}
</div>
</div>
@ -142,7 +159,7 @@ export const FDNewsletterBlockComponent: React.FC<FDNewsletterBlockProps> = ({
<>
<div className={`${isInline ? 'text-center md:text-left' : 'text-center'} mb-8 md:mb-12`}>
{heading && (
<h2 className={`font-joey-medium text-fd-h1 mb-4 ${headingColor}`}>
<h2 className={`font-joey-heavy text-fd-h1 mb-4 ${headingColor}`}>
{heading}
</h2>
)}
@ -166,7 +183,9 @@ export const FDNewsletterBlockComponent: React.FC<FDNewsletterBlockProps> = ({
<input type="email" required placeholder="Din e-postadress" value={email} onChange={(e) => setEmail(e.target.value)} className={`${inputClass} flex-1`} />
<button
onClick={handleSubmit}
disabled={!email || !consented || status === 'loading'}
disabled={isDisabled}
aria-disabled={isDisabled}
aria-describedby={isDisabled ? 'newsletter-inline-disabled-reason' : undefined}
className="fd-btn-primary whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed"
>
{status === 'loading' ? 'Skickar...' : buttonText}
@ -177,7 +196,9 @@ export const FDNewsletterBlockComponent: React.FC<FDNewsletterBlockProps> = ({
<input type="email" required placeholder="Din e-postadress" value={email} onChange={(e) => setEmail(e.target.value)} className={`${inputClass} mb-4`} />
<button
onClick={handleSubmit}
disabled={!email || !consented || status === 'loading'}
disabled={isDisabled}
aria-disabled={isDisabled}
aria-describedby={isDisabled ? 'newsletter-stacked-disabled-reason' : undefined}
className="fd-btn-primary w-full disabled:opacity-50 disabled:cursor-not-allowed"
>
{status === 'loading' ? 'Skickar...' : buttonText}
@ -185,12 +206,16 @@ export const FDNewsletterBlockComponent: React.FC<FDNewsletterBlockProps> = ({
</>
)}
{isDisabled && disabledReason && (
<p id={`newsletter-${isInline ? 'inline' : 'stacked'}-disabled-reason`} className="sr-only">{disabledReason}</p>
)}
<div className="mt-4">
<ConsentCheckbox consented={consented} setConsented={setConsented} consentText={consentText} privacyPolicyLink={privacyPolicyLink} isDark={isDark} bodyColor={bodyColor} />
</div>
{status === 'error' && (
<p className="text-red-400 text-fd-small mt-3 font-joey">{errorMsg}</p>
<p className="text-red-400 text-fd-small mt-3 font-joey" role="alert">{errorMsg}</p>
)}
</div>
</>

View File

@ -26,7 +26,8 @@ export const FDPartnersLogosBlockComponent: React.FC<FDPartnersLogosBlockProps>
<section className={`w-full py-16 md:py-20 lg:py-[99px] ${bgClass}`}>
<div className="max-w-[1200px] mx-auto px-6 md:px-8">
{heading && (
<h2 className={`font-joey-medium text-fd-h2 text-center mb-10 md:mb-14 ${titleClass}`}>
/* Priority #6: Was font-joey-medium — now font-joey-heavy for section heading consistency */
<h2 className={`font-joey-heavy text-fd-h2 text-center mb-10 md:mb-14 ${titleClass}`}>
{heading}
</h2>
)}

View File

@ -21,7 +21,7 @@ const cardStyleMap: Record<string, {
}> = {
outlined: {
bg: 'bg-white dark:bg-white/10',
border: 'border-[2px] border-gray-200 dark:border-white/20',
border: 'border-[5px] border-gray-200 dark:border-white/20',
title: 'text-fd-navy dark:text-fd-yellow',
subtitle: 'text-fd-navy dark:text-white',
body: 'text-fd-navy/80 dark:text-white/80',
@ -69,6 +69,8 @@ const gridColsMap: Record<number, string> = {
3: 'lg:grid-cols-3',
}
const cardRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
export const FDPricingCardBlockComponent: React.FC<FDPricingCardBlockProps> = ({
sectionTitle,
cards,
@ -97,7 +99,7 @@ export const FDPricingCardBlockComponent: React.FC<FDPricingCardBlockProps> = ({
{cards?.map((card, index) => (
<div
key={index}
className={`${style.bg} ${style.border} rounded-[70px] px-8 md:px-10 py-10 md:py-12 flex flex-col gap-5`}
className={`${style.bg} ${style.border} ${cardRadius} px-8 md:px-12 py-10 md:py-14 flex flex-col gap-5`}
>
<h3 className={`font-joey-heavy text-fd-h2 leading-tight ${style.title}`}>
{card.title}

View File

@ -0,0 +1,371 @@
'use client'
import React, { useState, useMemo } from 'react'
import type { FDServiceCalculatorBlock as Props } from '@/payload-types'
const formatKr = (n: number) => Math.round(n).toLocaleString('sv-SE') + ' kr'
const cardRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
/* ── Toggle switch ─────────────────────────────────────────────────────── */
function Toggle({ active, onToggle, label }: { active: boolean; onToggle: () => void; label: string }) {
return (
<button
type="button"
onClick={onToggle}
style={{ display: 'block', width: '3.5rem', height: '2rem', flexShrink: 0 }}
className={`relative rounded-full transition-all duration-300 ${active ? 'bg-fd-yellow' : 'bg-[#e2e8f0] dark:bg-white/20'}`}
role="switch"
aria-checked={active}
aria-label={label}
>
<div className={`absolute top-1 w-6 h-6 rounded-full bg-white shadow-sm transition-all duration-300 ${active ? 'left-7' : 'left-1'}`} />
</button>
)
}
/* ── +/- stepper row ───────────────────────────────────────────────────── */
function ResourceRow({
label, value, onChange, min = 0, max = 999, step = 1, unit, priceLabel, isDark,
}: {
label: string; value: number; onChange: (v: number) => void
min?: number; max?: number; step?: number; unit: string; priceLabel: string; isDark: boolean
}) {
return (
<div className="flex items-center justify-between py-4 border-b border-fd-navy/10 dark:border-white/10">
<div className="flex flex-col">
<span className="font-joey-bold text-fd-body-lg text-fd-navy dark:text-white">{label}</span>
<span className="font-joey text-fd-body text-fd-navy/50 dark:text-white/50">{priceLabel}</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => onChange(Math.max(min, value - step))}
className="w-11 h-11 rounded-full border-2 flex items-center justify-center font-joey-bold text-lg transition-colors border-fd-navy/20 text-fd-navy hover:border-fd-navy/60 dark:border-white/30 dark:text-white dark:hover:border-white/70"
aria-label={`Minska ${label}`}
></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-lg rounded-full px-2 py-1.5 border-2 bg-fd-surface-alt border-fd-navy/15 text-fd-navy dark:bg-white/10 dark:border-white/20 dark:text-white [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
style={{ appearance: 'textfield' }}
aria-label={label}
/>
<span className="font-joey text-fd-body w-8 text-fd-navy/40 dark:text-white/40">{unit}</span>
<button
type="button"
onClick={() => onChange(Math.min(max, value + step))}
className="w-11 h-11 rounded-full border-2 flex items-center justify-center font-joey-bold text-lg transition-colors border-fd-navy/20 text-fd-navy hover:border-fd-navy/60 dark:border-white/30 dark:text-white dark:hover:border-white/70"
aria-label={`Öka ${label}`}
>+</button>
</div>
</div>
)
}
/* ── Main component ────────────────────────────────────────────────────── */
export const FDServiceCalculatorBlockComponent: React.FC<Props> = ({
heading = 'Beräkna din kostnad',
description,
summaryHeading = 'Kostnadsöversikt',
totalLabel = 'Totalt per månad',
totalSuffix = 'exkl. moms',
orderCtaText = 'Beställ',
orderCtaLink = '/kontakt',
contactCtaText = 'Frågor? Kontakta oss',
contactCtaLink = '/kontakt',
sectionBackground = 'white',
optionGroups = [],
resources = [],
addons = [],
fixedFees = [],
discountPercent,
discountLabel,
}) => {
/* ── State: one selected index per option group ──────────────────────── */
const [selectedOptions, setSelectedOptions] = useState<Record<number, number>>(() => {
const initial: Record<number, number> = {}
;(optionGroups ?? []).forEach((_, i) => { initial[i] = 0 })
return initial
})
/* ── State: one number per resource slider ───────────────────────────── */
const [resourceValues, setResourceValues] = useState<Record<number, number>>(() => {
const initial: Record<number, number> = {}
;(resources ?? []).forEach((r, i) => { initial[i] = r.defaultValue ?? 0 })
return initial
})
/* ── State: toggle map for addons ────────────────────────────────────── */
const [addonToggles, setAddonToggles] = useState<Record<number, boolean>>({})
const toggleAddon = (i: number) => setAddonToggles((p) => ({ ...p, [i]: !p[i] }))
const discount = (discountPercent ?? 0) / 100
const disc = (v: number) => v * (1 - discount)
/* ── Derived costs ───────────────────────────────────────────────────── */
const costs = useMemo(() => {
// Option groups
const optionCosts = (optionGroups ?? []).map((group, gi) => {
const selectedIdx = selectedOptions[gi] ?? 0
const option = group.options?.[selectedIdx]
return disc(option?.price ?? 0)
})
// Resources
const resourceCosts = (resources ?? []).map((r, i) => {
return disc((resourceValues[i] ?? 0) * (r.pricePerUnit ?? 0))
})
// Addons
const addonCosts = (addons ?? []).map((a, i) => {
return addonToggles[i] ? disc(a.price ?? 0) : 0
})
// Fixed fees (not discounted)
const fixedTotal = (fixedFees ?? []).reduce((sum, f) => sum + (f.amount ?? 0), 0)
const total =
optionCosts.reduce((a, b) => a + b, 0) +
resourceCosts.reduce((a, b) => a + b, 0) +
addonCosts.reduce((a, b) => a + b, 0) +
fixedTotal
return { optionCosts, resourceCosts, addonCosts, fixedTotal, total }
}, [selectedOptions, resourceValues, addonToggles, optionGroups, resources, addons, fixedFees, discount])
/* ── Theme ───────────────────────────────────────────────────────────── */
const isDark = sectionBackground === 'navy'
const bgClass = isDark ? 'bg-fd-navy' : sectionBackground === 'gray' ? 'bg-fd-surface-alt dark:bg-fd-navy' : 'bg-white dark:bg-fd-navy'
const cardClass = isDark
? `bg-white/5 border-[5px] border-white/10 ${cardRadius}`
: `bg-white border-[5px] border-[#e2e8f0] ${cardRadius} dark:bg-white/5 dark:border-white/10`
const headingColor = isDark ? 'text-white' : 'text-fd-navy dark:text-white'
const descColor = isDark ? 'text-white/60' : 'text-fd-navy/60 dark:text-white/60'
const sectionLabel = isDark ? 'text-white/40' : 'text-fd-navy/35 dark:text-white/40'
const summaryLabel = isDark ? 'text-white' : 'text-fd-navy dark:text-white'
const categoryLabel = isDark ? 'text-white/40' : 'text-fd-navy/40 dark:text-white/40'
const dividerClass = isDark ? 'border-white/10' : 'border-fd-navy/8 dark:border-white/10'
const optActiveClass = 'bg-fd-yellow text-fd-navy border-fd-yellow font-joey-bold'
const optInactiveClass = isDark
? 'bg-transparent text-white/70 border-white/20 hover:border-white/50 font-joey'
: 'bg-transparent text-fd-navy/70 border-fd-navy/20 hover:border-fd-navy/50 font-joey dark:text-white/70 dark:border-white/20 dark:hover:border-white/50'
const discStr = discount > 0 ? ` (${discountPercent}% rabatt)` : ''
const hasAddons = (addons ?? []).length > 0
const hasFixedFees = (fixedFees ?? []).length > 0
/* ── Build summary rows ──────────────────────────────────────────────── */
const summaryRows: { label: string; cost: number; category?: string }[] = []
// Option group costs
;(optionGroups ?? []).forEach((group, gi) => {
const selectedIdx = selectedOptions[gi] ?? 0
const option = group.options?.[selectedIdx]
if (option && (option.price ?? 0) > 0) {
summaryRows.push({
label: `${group.groupLabel} (${option.label})`,
cost: costs.optionCosts[gi],
})
}
})
// Resource costs
;(resources ?? []).forEach((r, i) => {
const val = resourceValues[i] ?? 0
if (val > 0) {
const template = r.summaryTemplate || `${r.label} ({value} {unit})`
const label = template
.replace('{value}', String(val))
.replace('{unit}', r.unit || '')
summaryRows.push({ label, cost: costs.resourceCosts[i] })
}
})
// Fixed fees
;(fixedFees ?? []).forEach((f) => {
summaryRows.push({ label: f.label, cost: f.amount ?? 0 })
})
// Active addons
const addonSummaryRows = (addons ?? []).flatMap((a, i) =>
addonToggles[i] ? [{ label: a.label, cost: costs.addonCosts[i] }] : [],
)
const resolvedDiscountLabel = discountLabel
? discountLabel.replace('{percent}', String(discountPercent ?? 0))
: `${discountPercent}% rabatt på alla resurser`
return (
<section className={`fd-section ${bgClass}`}>
<div className="fd-container">
{/* ── Header ──────────────────────────────────────────────────── */}
{(heading || description) && (
<div className="text-center mb-10 md:mb-14">
{heading && <h2 className={`font-joey-heavy text-fd-h1 mb-3 ${headingColor}`}>{heading}</h2>}
{description && <p className={`font-joey text-fd-body-lg max-w-[600px] mx-auto ${descColor}`}>{description}</p>}
{discount > 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">
{resolvedDiscountLabel}
</span>
)}
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6 lg:gap-8">
{/* ── Left: Configuration ─────────────────────────────────── */}
<div className={`lg:col-span-3 p-7 md:p-9 ${cardClass}`}>
{/* Option groups */}
{(optionGroups ?? []).map((group, gi) => (
<div key={gi} className={gi > 0 ? 'mt-7' : ''}>
<p className={`font-joey-medium text-fd-body tracking-widest uppercase mb-3 ${sectionLabel}`}>
{group.groupLabel}
</p>
<div className="flex gap-3 mb-6 flex-wrap">
{group.options?.map((opt, oi) => (
<button
key={oi}
type="button"
onClick={() => setSelectedOptions((p) => ({ ...p, [gi]: oi }))}
className={`flex-1 min-w-[120px] py-3 rounded-full text-fd-body-lg border-2 transition-all duration-200 ${
selectedOptions[gi] === oi ? optActiveClass : optInactiveClass
}`}
>
{opt.label}
{(opt.price ?? 0) > 0 && (
<span className="ml-1 opacity-70">(+{opt.price}kr)</span>
)}
{(opt.price ?? 0) === 0 && group.options && group.options.some((o) => (o.price ?? 0) > 0) && (
<span className="ml-1 opacity-70">(gratis)</span>
)}
</button>
))}
</div>
</div>
))}
{/* Resource sliders */}
{(resources ?? []).length > 0 && (
<div>
{(resources ?? []).map((r, i) => (
<ResourceRow
key={i}
label={r.label}
value={resourceValues[i] ?? 0}
onChange={(v) => setResourceValues((p) => ({ ...p, [i]: v }))}
min={r.min ?? 0}
max={r.max ?? 1000}
step={r.step ?? 1}
unit={r.unit || ''}
priceLabel={`${r.pricePerUnit} kr/${r.unit}${discStr}`}
isDark={isDark}
/>
))}
</div>
)}
{/* Fixed fees (display only) */}
{hasFixedFees && (
<div className="mt-2">
{(fixedFees ?? []).map((f, i) => (
<div key={i} className={`flex items-center justify-between py-4 border-b ${dividerClass}`}>
<span className={`font-joey-bold text-fd-body-lg ${summaryLabel}`}>{f.label}</span>
<span className={`font-joey-medium text-fd-body-lg ${categoryLabel}`}>{formatKr(f.amount ?? 0)}</span>
</div>
))}
</div>
)}
{/* Toggle addons */}
{hasAddons && (
<div className="mt-7">
<p className={`font-joey-medium text-fd-body tracking-widest uppercase mb-1 ${sectionLabel}`}>
Tillvalstjänster
</p>
{(addons ?? []).map((addon, i) => (
<div key={i} className={`flex items-center gap-4 py-4 border-b ${dividerClass}`}>
<div className="flex-1 min-w-0">
<span className={`font-joey-bold text-fd-body-lg ${summaryLabel}`}>{addon.label}</span>
{addon.price != null && (
<span className={`block font-joey text-fd-body-lg ${categoryLabel}`}>{addon.price} kr/mån</span>
)}
{addon.description && (
<span className={`block font-joey text-fd-body-lg ${categoryLabel} mt-0.5`}>{addon.description}</span>
)}
</div>
<div className="flex-shrink-0 w-14">
<Toggle active={!!addonToggles[i]} onToggle={() => toggleAddon(i)} label={addon.label} />
</div>
</div>
))}
</div>
)}
</div>
{/* ── Right: Summary ──────────────────────────────────────── */}
<div className={`lg:col-span-2 p-7 md:p-9 flex flex-col ${cardClass}`}>
<h3 className={`font-joey-heavy text-fd-h2 mb-6 ${headingColor}`}>{summaryHeading}</h3>
<div className="flex-1 space-y-0">
{summaryRows.map((row, i) => (
<div key={i} className={`flex justify-between items-center py-2.5 border-b ${dividerClass}`}>
<span className={`font-joey text-fd-body-lg ${summaryLabel}`}>{row.label}</span>
<span className={`font-joey-bold text-fd-body-lg ${summaryLabel}`}>{formatKr(row.cost)}</span>
</div>
))}
{addonSummaryRows.length > 0 && (
<>
<div className="pt-4 pb-1">
<span className={`font-joey text-fd-body-lg ${categoryLabel}`}>Tillval</span>
</div>
{addonSummaryRows.map((row, i) => (
<div key={i} className={`flex justify-between items-center py-2.5 border-b ${dividerClass}`}>
<span className={`font-joey text-fd-body-lg ${summaryLabel}`}>{row.label}</span>
<span className={`font-joey-bold text-fd-body-lg ${summaryLabel}`}>{formatKr(row.cost)}</span>
</div>
))}
</>
)}
{summaryRows.length === 0 && addonSummaryRows.length === 0 && (
<p className={`font-joey text-fd-body-lg py-4 opacity-50 ${summaryLabel}`}>
Konfigurera din tjänst för att se kostnaden.
</p>
)}
</div>
<div className={`mt-6 pt-5 border-t-2 ${dividerClass}`}>
<div className="flex justify-between items-baseline">
<span className={`font-joey text-fd-body-lg ${summaryLabel}`}>{totalLabel}</span>
<span className={`font-joey-heavy text-fd-h1 ${headingColor}`}>{formatKr(costs.total)}</span>
</div>
{totalSuffix && (
<p className={`text-right font-joey text-fd-body-lg mt-1 ${categoryLabel}`}>{totalSuffix}</p>
)}
</div>
<div className="mt-7 space-y-3">
<a href={orderCtaLink || '#'} className="fd-btn-primary w-full justify-center">{orderCtaText}</a>
<a
href={contactCtaLink || '#'}
className={`${isDark ? 'fd-btn-secondary-dark' : 'fd-btn-secondary dark:fd-btn-secondary-dark'} w-full justify-center`}
>
{contactCtaText}
</a>
</div>
</div>
</div>
</div>
</section>
)
}

View File

@ -0,0 +1,196 @@
import type { Block } from 'payload'
export const FDServiceCalculatorBlock: Block = {
slug: 'fdServiceCalculator',
imageURL: '/block-thumbnails/fd-vps-calculator.png',
imageAltText: 'FD Tjänstekalkylator',
interfaceName: 'FDServiceCalculatorBlock',
labels: { singular: 'FD Tjänstekalkylator', plural: 'FD Tjänstekalkylatorer' },
fields: [
// ─── Presentation ──────────────────────────────────────────────────────
{
name: 'heading',
type: 'text',
label: 'Rubrik',
defaultValue: 'Beräkna din kostnad',
},
{
name: 'description',
type: 'text',
label: 'Beskrivning (valfri)',
},
{
name: 'summaryHeading',
type: 'text',
label: 'Rubrik — kostnadsöversikt',
defaultValue: 'Kostnadsöversikt',
},
{
name: 'totalLabel',
type: 'text',
label: 'Total-etikett',
defaultValue: 'Totalt per månad',
},
{
name: 'totalSuffix',
type: 'text',
label: 'Suffix under totalen',
defaultValue: 'exkl. moms',
},
{
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' },
],
},
{
type: 'row',
fields: [
{ name: 'contactCtaText', type: 'text', label: 'Kontakt-knapp text', defaultValue: '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 (mörkt)', value: 'navy' },
],
},
// ─── Option groups (e.g. OS selector, tier picker) ─────────────────────
{
name: 'optionGroups',
type: 'array',
label: 'Alternativgrupper',
admin: {
description: 'T.ex. "Operativsystem" med Linux/Windows, eller "Nivå" med Standard/Premium. Visas som knappar.',
},
fields: [
{ name: 'groupLabel', type: 'text', label: 'Gruppnamn', required: true },
{
name: 'options',
type: 'array',
label: 'Alternativ',
minRows: 2,
fields: [
{ name: 'label', type: 'text', label: 'Alternativtext', required: true },
{
name: 'price',
type: 'number',
label: 'Pris (kr/mån)',
defaultValue: 0,
admin: { description: '0 för gratis (t.ex. Linux)' },
},
],
},
],
},
// ─── Resource sliders (e.g. CPU, RAM, Storage) ─────────────────────────
{
name: 'resources',
type: 'array',
label: 'Resurser (reglage)',
admin: {
description: 'Varje rad blir ett +/- reglage. T.ex. CPU-kärnor, RAM (GB), Lagring (GB).',
},
fields: [
{
type: 'row',
fields: [
{ name: 'label', type: 'text', label: 'Namn', required: true },
{ name: 'unit', type: 'text', label: 'Enhet', required: true, defaultValue: 'GB', admin: { width: '25%' } },
],
},
{
type: 'row',
fields: [
{ name: 'pricePerUnit', type: 'number', label: 'Pris per enhet (kr)', required: true },
{ name: 'defaultValue', type: 'number', label: 'Startvärde', defaultValue: 0 },
],
},
{
type: 'row',
fields: [
{ name: 'min', type: 'number', label: 'Min', defaultValue: 0 },
{ name: 'max', type: 'number', label: 'Max', defaultValue: 1000 },
{ name: 'step', type: 'number', label: 'Steg', defaultValue: 1 },
],
},
{
name: 'summaryTemplate',
type: 'text',
label: 'Sammanfattningsmall (valfri)',
admin: {
description: 'Använd {value} och {unit} som variabler. T.ex. "SSD NVMe ({value} {unit})".',
},
},
],
},
// ─── Toggle add-ons ────────────────────────────────────────────────────
{
name: 'addons',
type: 'array',
label: 'Tillvalstjänster',
admin: {
description: 'Kunden kan slå på/av dessa med en toggle.',
},
fields: [
{ name: 'label', type: 'text', label: 'Tjänstnamn', required: true },
{ name: 'price', type: 'number', label: 'Pris (kr/mån)', required: true },
{
name: 'description',
type: 'text',
label: 'Kort beskrivning (valfri)',
},
],
},
// ─── Fixed fees ────────────────────────────────────────────────────────
{
name: 'fixedFees',
type: 'array',
label: 'Fasta avgifter',
admin: {
description: 'Alltid inkluderade i totalen. Kunden kan inte välja bort dessa.',
},
fields: [
{ name: 'label', type: 'text', label: 'Avgiftsnamn', required: true },
{ name: 'amount', type: 'number', label: 'Belopp (kr/mån)', required: true },
],
},
// ─── Discount ──────────────────────────────────────────────────────────
{
type: 'collapsible',
label: 'Kampanjrabatt',
admin: { initCollapsed: true },
fields: [
{
name: 'discountPercent',
type: 'number',
label: 'Rabatt (%)',
min: 0,
max: 100,
admin: { description: 'Appliceras på alla resurser och tillval. 0 = ingen rabatt.' },
},
{
name: 'discountLabel',
type: 'text',
label: 'Rabattetikett (visas som badge)',
admin: {
description: 'T.ex. "{percent}% rabatt på alla resurser". Använd {percent} som variabel.',
condition: (_, siblingData) => (siblingData?.discountPercent ?? 0) > 0,
},
},
],
},
],
}

View File

@ -1,7 +1,10 @@
'use client'
import React, { useState, useRef } from 'react'
import React, { useState, useRef, useEffect } from 'react'
import type { FDServiceChooserBlock as Props } from '@/payload-types'
/* Consistent radius system — same as CardGrid, PricingCard, etc. */
const cardRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
export const FDServiceChooserBlockComponent: React.FC<Props> = ({
heading,
description,
@ -10,16 +13,33 @@ export const FDServiceChooserBlockComponent: React.FC<Props> = ({
}) => {
const [activeIndex, setActiveIndex] = useState(0)
const [animating, setAnimating] = useState(false)
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false)
const prevIndex = useRef(0)
/* Priority #8: Detect prefers-reduced-motion */
useEffect(() => {
const mql = window.matchMedia('(prefers-reduced-motion: reduce)')
setPrefersReducedMotion(mql.matches)
const handler = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches)
mql.addEventListener('change', handler)
return () => mql.removeEventListener('change', handler)
}, [])
const isDark = sectionBackground === 'navy'
const bgClass = isDark ? 'bg-fd-navy' : sectionBackground === 'gray' ? 'bg-fd-surface-alt dark:bg-fd-navy' : 'bg-white dark:bg-fd-navy'
const titleClass = isDark ? 'text-fd-yellow' : 'text-fd-navy dark:text-fd-yellow'
const bodyClass = isDark ? 'text-white' : 'text-fd-navy dark:text-white'
const cardBg = isDark ? 'bg-white/10 border border-white/10' : 'bg-white border border-fd-navy/10 dark:bg-white/10 dark:border-white/10'
const cardBg = isDark ? 'bg-white/10 border-[5px] border-white/10' : 'bg-white border-[5px] border-gray-200 dark:bg-white/10 dark:border-white/10'
const handleTabChange = (i: number) => {
if (i === activeIndex) return
if (prefersReducedMotion) {
/* Instant switch — no animation */
setActiveIndex(i)
return
}
setAnimating(true)
prevIndex.current = activeIndex
setTimeout(() => {
@ -66,7 +86,11 @@ export const FDServiceChooserBlockComponent: React.FC<Props> = ({
</div>
<div
className={`transition-all duration-200 ${animating ? 'opacity-0 translate-y-2' : 'opacity-100 translate-y-0'}`}
className={
prefersReducedMotion
? '' /* No transition when motion is reduced */
: `transition-all duration-200 ${animating ? 'opacity-0 translate-y-2' : 'opacity-100 translate-y-0'}`
}
style={{ minHeight: '200px' }}
>
{activeCategory?.intro && (
@ -79,13 +103,17 @@ export const FDServiceChooserBlockComponent: React.FC<Props> = ({
{activeCategory?.services?.map((service, i) => (
<div
key={i}
className={`rounded-[30px] md:rounded-[40px] p-8 md:p-10 flex flex-col gap-4 ${cardBg}`}
style={{
transition: 'opacity 0.3s, transform 0.3s',
transitionDelay: `${i * 60}ms`,
opacity: animating ? 0 : 1,
transform: animating ? 'translateY(8px)' : 'translateY(0)',
}}
className={`${cardRadius} p-8 md:p-12 flex flex-col gap-4 ${cardBg}`}
style={
prefersReducedMotion
? undefined
: {
transition: 'opacity 0.3s, transform 0.3s',
transitionDelay: `${i * 60}ms`,
opacity: animating ? 0 : 1,
transform: animating ? 'translateY(8px)' : 'translateY(0)',
}
}
>
<h3 className={`font-joey-bold text-fd-h3 ${isDark ? 'text-fd-yellow' : 'text-fd-navy dark:text-fd-yellow'}`}>
{service.title}

View File

@ -8,6 +8,9 @@ const columnClasses: Record<string, string> = {
'4': 'grid-cols-2 lg:grid-cols-4',
}
/* Priority #5: Responsive radius for service images */
const imageRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
export const FDServicesGridBlockComponent: React.FC<FDServicesGridBlockProps> = ({
heading,
services,
@ -34,7 +37,7 @@ export const FDServicesGridBlockComponent: React.FC<FDServicesGridBlockProps> =
<FDImage
media={media}
size="medium"
className="w-full aspect-square object-cover rounded-[70px]"
className={`w-full aspect-square object-cover ${imageRadius}`}
sizes={columns === '2' ? '(max-width: 768px) 100vw, 550px' : '(max-width: 768px) 50vw, 280px'}
fallbackAlt={service.title}
/>

View File

@ -9,16 +9,34 @@ export const FDStatisticsBlockComponent: React.FC<Props> = ({
numberColor = 'gradient',
}) => {
const [visible, setVisible] = useState(false)
/* Priority #8: Detect prefers-reduced-motion */
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
/* Check motion preference at mount */
const mql = window.matchMedia('(prefers-reduced-motion: reduce)')
setPrefersReducedMotion(mql.matches)
const handler = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches)
mql.addEventListener('change', handler)
return () => mql.removeEventListener('change', handler)
}, [])
useEffect(() => {
/* If user prefers reduced motion, show everything immediately */
if (prefersReducedMotion) {
setVisible(true)
return
}
const observer = new IntersectionObserver(
([entry]) => { if (entry.isIntersecting) setVisible(true) },
{ threshold: 0.3 },
)
if (ref.current) observer.observe(ref.current)
return () => observer.disconnect()
}, [])
}, [prefersReducedMotion])
const bgClass =
sectionBackground === 'navy' ? 'bg-fd-navy' :
@ -54,10 +72,14 @@ export const FDStatisticsBlockComponent: React.FC<Props> = ({
{(stats ?? []).map((stat, i) => (
<div
key={i}
className={`flex flex-col items-center gap-2 transition-all duration-700 ${
visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'
className={`flex flex-col items-center gap-2 ${
prefersReducedMotion
? '' /* No animation classes at all when motion is reduced */
: `transition-all duration-700 ${
visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'
}`
}`}
style={{ transitionDelay: `${i * 120}ms` }}
style={prefersReducedMotion ? undefined : { transitionDelay: `${i * 120}ms` }}
>
<span className={`font-joey-heavy text-5xl md:text-7xl lg:text-[96px] leading-none ${getNumberClass()}`}>
{stat.number}

View File

@ -38,6 +38,9 @@ const colsMap: Record<string, string> = {
'4': 'sm:grid-cols-2 lg:grid-cols-4',
}
/* Priority #5: Responsive radius for team member cards */
const cardRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
export const FDTeamBlockComponent: React.FC<FDTeamBlockProps> = ({
heading,
subheading,
@ -76,7 +79,7 @@ export const FDTeamBlockComponent: React.FC<FDTeamBlockProps> = ({
const hasPhoto = photo && typeof photo === 'object' && photo.url
return (
<div key={i} className={`${card.bg} rounded-[70px] overflow-hidden flex flex-col`}>
<div key={i} className={`${card.bg} ${cardRadius} overflow-hidden flex flex-col`}>
<div className="aspect-[4/3] w-full overflow-hidden">
{hasPhoto ? (

View File

@ -32,6 +32,8 @@ const bgMap: Record<string, {
},
}
const cardRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
const Avatar: React.FC<{ media: Media | undefined; name: string; size: number }> = ({ media, name, size }) => {
if (!media?.url) return null
return (
@ -76,7 +78,7 @@ export const FDTestimonialBlockComponent: React.FC<FDTestimonialBlockProps> = ({
const t = testimonials[0]
const avatar = t.avatar as Media | undefined
return (
<div className={`${theme.card} rounded-[70px] px-8 md:px-16 py-10 md:py-16 flex flex-col gap-8`}>
<div className={`${theme.card} ${cardRadius} px-8 md:px-16 py-10 md:py-16 flex flex-col gap-8`}>
<p className={`font-joey-medium text-xl md:text-2xl lg:text-3xl leading-relaxed ${theme.quote}`}>
&ldquo;{t.quote}&rdquo;
</p>
@ -98,7 +100,7 @@ export const FDTestimonialBlockComponent: React.FC<FDTestimonialBlockProps> = ({
{testimonials.slice(1).map((t, i) => {
const avatar = t.avatar as Media | undefined
return (
<div key={i} className={`${theme.card} rounded-[70px] px-8 md:px-12 py-10 md:py-12 flex flex-col gap-6`}>
<div key={i} className={`${theme.card} ${cardRadius} px-8 md:px-12 py-10 md:py-12 flex flex-col gap-6`}>
<p className={`font-joey-medium text-lg md:text-xl leading-relaxed ${theme.quote}`}>
&ldquo;{t.quote}&rdquo;
</p>
@ -122,7 +124,7 @@ export const FDTestimonialBlockComponent: React.FC<FDTestimonialBlockProps> = ({
{testimonials?.map((t, i) => {
const avatar = t.avatar as Media | undefined
return (
<div key={i} className={`${theme.card} rounded-[70px] px-8 md:px-10 py-10 md:py-12 flex flex-col gap-6`}>
<div key={i} className={`${theme.card} ${cardRadius} px-8 md:px-10 py-10 md:py-12 flex flex-col gap-6`}>
<span className={`font-joey-heavy text-5xl leading-none ${theme.accent} opacity-30`}>
&ldquo;
</span>

View File

@ -1,102 +0,0 @@
import type { Block } from 'payload'
import {
lexicalEditor,
BoldFeature,
ItalicFeature,
UnderlineFeature,
LinkFeature,
UnorderedListFeature,
OrderedListFeature,
HeadingFeature,
BlockquoteFeature,
} from '@payloadcms/richtext-lexical'
const fdRichTextEditor = lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BoldFeature(),
ItalicFeature(),
UnderlineFeature(),
LinkFeature({ enabledCollections: ['pages', 'posts'] }),
UnorderedListFeature(),
OrderedListFeature(),
HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
BlockquoteFeature(),
],
})
export const FDTextBlock: Block = {
slug: 'fdText',
interfaceName: 'FDTextBlock',
labels: {
singular: 'FD Textblock',
plural: 'FD Textblock',
},
fields: [
{
name: 'heading',
type: 'text',
localized: true,
label: 'Rubrik',
},
{
name: 'subheading',
type: 'text',
localized: true,
label: 'Underrubrik',
},
{
name: 'body',
type: 'richText',
localized: true,
label: 'Brödtext',
editor: fdRichTextEditor,
},
{
name: 'alignment',
type: 'select',
label: 'Textjustering',
defaultValue: 'left',
options: [
{ label: 'Vänster', value: 'left' },
{ label: 'Centrerad', value: 'center' },
{ label: 'Höger', value: 'right' },
],
},
{
name: 'textColor',
type: 'select',
label: 'Textfärg',
defaultValue: 'navy',
options: [
{ label: 'Navy', value: 'navy' },
{ label: 'Vit', value: 'white' },
{ label: 'Gul', value: 'yellow' },
],
},
{
name: 'sectionBackground',
type: 'select',
label: 'Bakgrund',
defaultValue: 'white',
options: [
{ label: 'Vit', value: 'white' },
{ label: 'Navy', value: 'navy' },
{ label: 'Grå', value: 'gray' },
{ label: 'Gul', value: 'yellow' },
],
},
{
name: 'maxWidth',
type: 'select',
label: 'Maxbredd',
defaultValue: 'wide',
options: [
{ label: 'Smal (600px)', value: 'narrow' },
{ label: 'Medium (800px)', value: 'medium' },
{ label: 'Bred (1100px)', value: 'wide' },
{ label: 'Full', value: 'full' },
],
},
],
}

View File

@ -41,6 +41,9 @@ function extractVimeoId(url: string): string | null {
return match ? match[1] : null
}
/* Priority #5: Responsive video radius */
const videoRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
export const FDVideoBlockComponent: React.FC<FDVideoBlockProps> = ({
heading,
description,
@ -96,6 +99,9 @@ export const FDVideoBlockComponent: React.FC<FDVideoBlockProps> = ({
const handlePlay = useCallback(() => { setIsPlaying(true) }, [])
/* Accessibility: include video title in aria-label */
const videoTitle = heading || 'Video'
const PlayButton = () => (
<div className="absolute inset-0 flex items-center justify-center bg-black/20 group-hover:bg-black/30 transition-colors">
<div className="w-20 h-20 md:w-24 md:h-24 rounded-full bg-fd-yellow flex items-center justify-center shadow-xl group-hover:scale-110 transition-transform">
@ -113,7 +119,8 @@ export const FDVideoBlockComponent: React.FC<FDVideoBlockProps> = ({
{(heading || description) && (
<div className="mb-8 md:mb-12 text-center">
{heading && (
<h2 className={`font-joey-medium text-fd-h1 mb-4 ${headingColor}`}>
/* Priority #6: Was font-joey-medium — all other section headings use font-joey-heavy */
<h2 className={`font-joey-heavy text-fd-h1 mb-4 ${headingColor}`}>
{heading}
</h2>
)}
@ -125,7 +132,8 @@ export const FDVideoBlockComponent: React.FC<FDVideoBlockProps> = ({
</div>
)}
<div className={`relative w-full overflow-hidden bg-black transition-all duration-300 ${isPlaying ? 'rounded-none' : 'rounded-[70px]'}`}>
{/* Priority #5: Responsive radius instead of fixed rounded-[70px] */}
<div className={`relative w-full overflow-hidden bg-black transition-all duration-300 ${isPlaying ? 'rounded-none' : videoRadius}`}>
<div className="relative w-full" style={{ paddingBottom: aspectRatio === '16/10' ? '62.5%' : '56.25%' }}>
{videoSource === 'upload' && videoMedia?.url && (
@ -143,7 +151,7 @@ export const FDVideoBlockComponent: React.FC<FDVideoBlockProps> = ({
Din webbläsare stöder inte videouppspelning.
</video>
{!autoplay && hasThumbnail && !isPlaying && (
<button onClick={handlePlay} className="absolute inset-0 w-full h-full z-10 cursor-pointer group" aria-label="Spela video">
<button onClick={handlePlay} className="absolute inset-0 w-full h-full z-10 cursor-pointer group" aria-label={`Spela video: ${videoTitle}`}>
<Image src={thumbnailUrl} alt={(thumbnailMedia as any)?.alt || 'Video miniatyrböld'} fill className="object-cover" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px" />
<PlayButton />
</button>
@ -154,14 +162,14 @@ export const FDVideoBlockComponent: React.FC<FDVideoBlockProps> = ({
{videoSource === 'youtube' && youtubeId && (
<>
{showThumbnailOverlay ? (
<button onClick={handlePlay} className="absolute inset-0 w-full h-full z-10 cursor-pointer group" aria-label="Spela YouTube-video">
<button onClick={handlePlay} className="absolute inset-0 w-full h-full z-10 cursor-pointer group" aria-label={`Spela YouTube-video: ${videoTitle}`}>
<Image src={thumbnailUrl || autoThumbnailUrl} alt={(thumbnailMedia as any)?.alt || 'Video miniatyrbild'} fill className="object-cover" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px" />
<PlayButton />
</button>
) : (
<iframe
src={`https://www.youtube.com/embed/${youtubeId}?autoplay=${isPlaying ? 1 : 0}&rel=0`}
title={heading || 'YouTube-video'}
title={videoTitle}
className="absolute inset-0 w-full h-full"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
@ -175,14 +183,14 @@ export const FDVideoBlockComponent: React.FC<FDVideoBlockProps> = ({
{videoSource === 'vimeo' && vimeoId && (
<>
{showThumbnailOverlay ? (
<button onClick={handlePlay} className="absolute inset-0 w-full h-full z-10 cursor-pointer group" aria-label="Spela Vimeo-video">
<button onClick={handlePlay} className="absolute inset-0 w-full h-full z-10 cursor-pointer group" aria-label={`Spela Vimeo-video: ${videoTitle}`}>
<Image src={thumbnailUrl} alt={(thumbnailMedia as any)?.alt || 'Video miniatyrbild'} fill className="object-cover" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px" />
<PlayButton />
</button>
) : (
<iframe
src={`https://player.vimeo.com/video/${vimeoId}?autoplay=${isPlaying ? 1 : 0}`}
title={heading || 'Vimeo-video'}
title={videoTitle}
className="absolute inset-0 w-full h-full"
allow="autoplay; fullscreen; picture-in-picture"
allowFullScreen

View File

@ -38,27 +38,27 @@ function ResourceRow({
return (
<div className="flex items-center justify-between py-4 border-b border-fd-navy/10 dark:border-white/10">
<div className="flex flex-col">
<span className="font-joey-bold text-fd-body text-fd-navy dark:text-white">{label}</span>
<span className="font-joey text-fd-small text-fd-navy/50 dark:text-white/50">{priceLabel}</span>
<span className="font-joey-bold text-fd-body-lg text-fd-navy dark:text-white">{label}</span>
<span className="font-joey text-fd-body text-fd-navy/50 dark:text-white/50">{priceLabel}</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 border-2 flex items-center justify-center font-joey-bold text-base transition-colors border-fd-navy/20 text-fd-navy hover:border-fd-navy/60 dark:border-white/30 dark:text-white dark:hover:border-white/70"
className="w-11 h-11 rounded-full border-2 flex items-center justify-center font-joey-bold text-lg transition-colors border-fd-navy/20 text-fd-navy hover:border-fd-navy/60 dark:border-white/30 dark:text-white dark:hover:border-white/70"
></button>
<input
type="number"
value={value}
onChange={(e) => onChange(Math.max(min, Math.min(max, Number(e.target.value) || 0)))}
className="w-16 text-center font-joey-medium text-fd-body rounded-full px-2 py-1.5 border-2 bg-fd-surface-alt border-fd-navy/15 text-fd-navy dark:bg-white/10 dark:border-white/20 dark:text-white [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
className="w-20 text-center font-joey-medium text-fd-body-lg rounded-full px-2 py-1.5 border-2 bg-fd-surface-alt border-fd-navy/15 text-fd-navy dark:bg-white/10 dark:border-white/20 dark:text-white [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
style={{ appearance: 'textfield' }}
/>
<span className="font-joey text-fd-small w-7 text-fd-navy/40 dark:text-white/40">{unit}</span>
<span className="font-joey text-fd-body w-8 text-fd-navy/40 dark:text-white/40">{unit}</span>
<button
type="button"
onClick={() => onChange(Math.min(max, value + step))}
className="w-9 h-9 rounded-full border-2 flex items-center justify-center font-joey-bold text-base transition-colors border-fd-navy/20 text-fd-navy hover:border-fd-navy/60 dark:border-white/30 dark:text-white dark:hover:border-white/70"
className="w-11 h-11 rounded-full border-2 flex items-center justify-center font-joey-bold text-lg transition-colors border-fd-navy/20 text-fd-navy hover:border-fd-navy/60 dark:border-white/30 dark:text-white dark:hover:border-white/70"
>+</button>
</div>
</div>
@ -120,8 +120,8 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
// Use Tailwind classes for card styling instead of inline styles
const cardClass = isDark
? 'bg-white/5 border-[6px] border-white/10 rounded-[clamp(28px,4vw,60px)]'
: 'bg-white border-[6px] border-[#e2e8f0] rounded-[clamp(28px,4vw,60px)] dark:bg-white/5 dark:border-white/10'
? 'bg-white/5 border-[5px] border-white/10 rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
: 'bg-white border-[5px] border-[#e2e8f0] rounded-[32px] md:rounded-[50px] lg:rounded-[70px] dark:bg-white/5 dark:border-white/10'
const headingColor = isDark ? 'text-white' : 'text-fd-navy dark:text-white'
const descColor = isDark ? 'text-white/60' : 'text-fd-navy/60 dark:text-white/60'
@ -173,7 +173,7 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
{/* Left: Config */}
<div className={`lg:col-span-3 p-7 md:p-9 ${cardClass}`}>
<p className={`font-joey-medium text-fd-small tracking-widest uppercase mb-3 ${sectionLabel}`}>
<p className={`font-joey-medium text-fd-body tracking-widest uppercase mb-3 ${sectionLabel}`}>
Operativsystem
</p>
<div className="flex gap-3 mb-6">
@ -182,7 +182,7 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
key={opt}
type="button"
onClick={() => setOs(opt)}
className={`flex-1 py-3 rounded-full text-fd-body border-2 transition-all duration-200 ${os === opt ? osActiveClass : osInactiveClass}`}
className={`flex-1 py-3 rounded-full text-fd-body-lg border-2 transition-all duration-200 ${os === opt ? osActiveClass : osInactiveClass}`}
>
{opt === 'linux' ? 'Linux (gratis)' : `Windows (+${pricing.windows}kr)`}
</button>
@ -198,20 +198,20 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
{showAdminFee && (
<div className={`flex items-center justify-between py-4 border-b ${dividerClass}`}>
<span className={`font-joey-bold text-fd-body ${summaryLabel}`}>Adminavgift</span>
<span className={`font-joey-medium text-fd-body ${categoryLabel}`}>{formatKr(feeAmount)}</span>
<span className={`font-joey-bold text-fd-body-lg ${summaryLabel}`}>Adminavgift</span>
<span className={`font-joey-medium text-fd-body-lg ${categoryLabel}`}>{formatKr(feeAmount)}</span>
</div>
)}
{hasTillval && (
<div className="mt-7">
<p className={`font-joey-medium text-fd-small tracking-widest uppercase mb-1 ${sectionLabel}`}>Tillvalstjänster</p>
<p className={`font-joey-medium text-fd-body tracking-widest uppercase mb-1 ${sectionLabel}`}>Tillvalstjänster</p>
{(additionalServices ?? []).map((svc, i) => (
<div key={i} className={`flex items-center gap-4 py-4 border-b ${dividerClass}`}>
<div className="flex-1 min-w-0">
<span className={`font-joey-bold text-fd-body ${summaryLabel}`}>{svc.label}</span>
<span className={`font-joey-bold text-fd-body-lg ${summaryLabel}`}>{svc.label}</span>
{svc.price != null && (
<span className={`block font-joey text-fd-small ${categoryLabel}`}>{svc.price} kr/mån</span>
<span className={`block font-joey text-fd-body-lg ${categoryLabel}`}>{svc.price} kr/mån</span>
)}
</div>
<div className="flex-shrink-0 w-14">
@ -230,20 +230,20 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
<div className="flex-1 space-y-0">
{baseRows.map((row, i) => (
<div key={i} className={`flex justify-between items-center py-2.5 border-b ${dividerClass}`}>
<span className={`font-joey text-fd-body ${summaryLabel}`}>{row.label}</span>
<span className={`font-joey-bold text-fd-body ${summaryLabel}`}>{formatKr(row.cost)}</span>
<span className={`font-joey text-fd-body-lg ${summaryLabel}`}>{row.label}</span>
<span className={`font-joey-bold text-fd-body-lg ${summaryLabel}`}>{formatKr(row.cost)}</span>
</div>
))}
{tillvalRows.length > 0 && (
<>
<div className="pt-4 pb-1">
<span className={`font-joey text-fd-small ${categoryLabel}`}>Tillval</span>
<span className={`font-joey text-fd-body-lg ${categoryLabel}`}>Tillval</span>
</div>
{tillvalRows.map((row, i) => (
<div key={i} className={`flex justify-between items-center py-2.5 border-b ${dividerClass}`}>
<span className={`font-joey text-fd-body ${summaryLabel}`}>{row.label}</span>
<span className={`font-joey-bold text-fd-body ${summaryLabel}`}>{formatKr(row.cost)}</span>
<span className={`font-joey text-fd-body-lg ${summaryLabel}`}>{row.label}</span>
<span className={`font-joey-bold text-fd-body-lg ${summaryLabel}`}>{formatKr(row.cost)}</span>
</div>
))}
</>
@ -252,10 +252,10 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
<div className={`mt-6 pt-5 border-t-2 ${dividerClass}`}>
<div className="flex justify-between items-baseline">
<span className={`font-joey text-fd-body ${summaryLabel}`}>Totalt per månad</span>
<span className={`font-joey text-fd-body-lg ${summaryLabel}`}>Totalt per månad</span>
<span className={`font-joey-heavy text-fd-h1 ${headingColor}`}>{formatKr(costs.total)}</span>
</div>
<p className={`text-right font-joey text-fd-small mt-1 ${categoryLabel}`}>exkl. moms</p>
<p className={`text-right font-joey text-fd-body-lg mt-1 ${categoryLabel}`}>exkl. moms</p>
</div>
<div className="mt-7 space-y-3">

View File

@ -22,6 +22,8 @@ const btnVariantMap: Record<string, { variant: 'primary' | 'outline' }> = {
white: { variant: 'primary' },
}
const cardRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
export const FDWideCardBlockComponent: React.FC<FDWideCardBlockProps> = ({
heading,
body,
@ -41,7 +43,7 @@ export const FDWideCardBlockComponent: React.FC<FDWideCardBlockProps> = ({
return (
<section className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}>
<div className="max-w-[1200px] mx-auto px-6 md:px-8">
<div className={`${card.bg} rounded-[70px] overflow-hidden flex flex-col lg:flex-row`}>
<div className={`${card.bg} ${cardRadius} overflow-hidden flex flex-col lg:flex-row`}>
<div className="flex-1 flex flex-col justify-center gap-5 md:gap-6 px-8 md:px-14 lg:px-16 py-12 md:py-16">
<h2 className={`font-joey-heavy text-fd-h1 leading-tight ${card.heading}`}>
{heading}

View File

@ -34,6 +34,7 @@ import { FDVideoBlockComponent } from '@/blocks/FDVideoBlock/Component'
import { FDCtaBannerBlockComponent } from './FDCtaBannerBlock/Component'
import { FDTestimonialBlockComponent } from './FDTestimonialBlock/Component'
import { FDTeamBlockComponent } from './FDTeamBlock/Component'
import { FDServiceCalculatorBlockComponent } from '@/blocks/FDServiceCalculatorBlock/Component'
const blockComponents: Record<string, React.FC<any>> = {
formBlock: FormBlock,
@ -68,42 +69,16 @@ const blockComponents: Record<string, React.FC<any>> = {
fdCtaBanner: FDCtaBannerBlockComponent,
fdTestimonial: FDTestimonialBlockComponent,
fdTeam: FDTeamBlockComponent,
fdServiceCalculator: FDServiceCalculatorBlockComponent,
}
// FD blocks handle their own full-width sections with internal padding
const fullWidthBlocks = new Set([
'fdHero',
'fdCtaSideImage',
'fdFeatureAnnouncement',
'fdServicesGrid',
'fdContact',
'fdFaq',
'fdCardGrid',
'fdPricingCard',
'fdSpacer',
'fdIconBar',
'fdUspChecklist',
'fdWideCard',
'fdTechProperties',
'fdUspTable',
'fdHeaderTextImage',
'fdText',
'fdContactForm',
'fdTags',
'fdLocationsGrid',
'fdAlternateHero',
'fdStatistics',
'fdPartnersLogos',
'fdNewsletter',
'fdServiceChooser',
'fdDataTable',
'fdVpsCalculator',
'fdCodeEmbed',
'fdVideo',
'fdCtaBanner',
'fdTestimonial',
'fdTeam',
])
/**
* Quick win: All FD blocks handle their own full-width sections,
* so instead of maintaining a manual Set we use a prefix check.
* This means new fdXxx blocks are automatically full-width.
*/
const isFullWidthBlock = (blockType: string): boolean =>
blockType.startsWith('fd')
export const RenderBlocks: React.FC<{
blocks: Page['layout'][0][]
@ -122,10 +97,8 @@ export const RenderBlocks: React.FC<{
const Block = blockComponents[blockType]
if (Block) {
const isFullWidth = fullWidthBlocks.has(blockType)
return (
<div className={isFullWidth ? '' : 'my-16'} key={index}>
<div className={isFullWidthBlock(blockType) ? '' : 'my-16'} key={index}>
<Block {...block} disableInnerContainer />
</div>
)

View File

@ -37,6 +37,7 @@ import { FDVideoBlock } from '../../blocks/FDVideoBlock/config'
import { FDCtaBannerBlock } from '../../blocks/FDCtaBannerBlock/config'
import { FDTestimonialBlock } from '../../blocks/FDTestimonialBlock/config'
import { FDTeamBlock } from '../../blocks/FDTeamBlock/config'
import { FDServiceCalculatorBlock } from '../../blocks/FDServiceCalculatorBlock/config'
import {
MetaDescriptionField,
@ -125,6 +126,7 @@ export const Pages: CollectionConfig<'pages'> = {
FDServiceChooserBlock,
FDDataTableBlock,
FDVpsCalculatorBlock,
FDServiceCalculatorBlock,
FDTagsBlock,
FDTextBlock,
FDCodeEmbedBlock,

View File

@ -57,27 +57,34 @@ export const AnnouncementBarComponent: React.FC<Props> = ({
const href = resolveUrl(buttonLink)
const newTab = buttonLink?.newTab ?? false
/*
* If a link is provided, the entire text becomes clickable.
* buttonLabel is appended to the text with an arrow if present,
* but it's all one inline link no alignment issues.
*/
const displayText = buttonLabel ? `${text} ${buttonLabel}` : text
return (
<div
className={`w-full px-10 py-2 relative flex items-center justify-center ${bgClass}`}
role="status"
>
{/* Centered content — px-10 leaves room for the dismiss button on both sides */}
<div className="flex items-center justify-center gap-3 text-sm font-joey text-center flex-wrap">
<span>{text}</span>
{buttonLabel && href && (
<div className="text-sm font-joey text-center">
{href ? (
<a
href={href}
target={newTab ? '_blank' : undefined}
rel={newTab ? 'noopener noreferrer' : undefined}
className="underline font-joey-bold hover:opacity-70 transition-opacity whitespace-nowrap"
className="hover:opacity-70 transition-opacity underline-offset-2 hover:underline"
>
{buttonLabel}
{displayText}
</a>
) : (
<span>{text}</span>
)}
</div>
{/* Dismiss — truly absolute so it doesn't affect centering */}
{/* Dismiss — absolutely positioned so it doesn't affect centering */}
{dismissible && (
<button
onClick={handleDismiss}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +0,0 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "site_settings" ADD COLUMN "matomo_enabled" boolean DEFAULT false;
ALTER TABLE "site_settings" ADD COLUMN "matomo_url" varchar;
ALTER TABLE "site_settings" ADD COLUMN "matomo_site_id" varchar;`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "site_settings" DROP COLUMN "matomo_enabled";
ALTER TABLE "site_settings" DROP COLUMN "matomo_url";
ALTER TABLE "site_settings" DROP COLUMN "matomo_site_id";`)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +0,0 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "site_settings" ADD COLUMN "matomo_code" varchar;
ALTER TABLE "site_settings" DROP COLUMN "matomo_url";
ALTER TABLE "site_settings" DROP COLUMN "matomo_site_id";`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "site_settings" ADD COLUMN "matomo_url" varchar;
ALTER TABLE "site_settings" ADD COLUMN "matomo_site_id" varchar;
ALTER TABLE "site_settings" DROP COLUMN "matomo_code";`)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,70 +0,0 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "pages_blocks_fd_contact_form_form_fields" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "_pages_v_blocks_fd_contact_form_form_fields" DISABLE ROW LEVEL SECURITY;
DROP TABLE "pages_blocks_fd_contact_form_form_fields" CASCADE;
DROP TABLE "_pages_v_blocks_fd_contact_form_form_fields" CASCADE;
ALTER TABLE "pages_blocks_fd_contact_form" ADD COLUMN "form_id" integer;
ALTER TABLE "_pages_v_blocks_fd_contact_form" ADD COLUMN "form_id" integer;
ALTER TABLE "pages_blocks_fd_contact_form" ADD CONSTRAINT "pages_blocks_fd_contact_form_form_id_forms_id_fk" FOREIGN KEY ("form_id") REFERENCES "public"."forms"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "_pages_v_blocks_fd_contact_form" ADD CONSTRAINT "_pages_v_blocks_fd_contact_form_form_id_forms_id_fk" FOREIGN KEY ("form_id") REFERENCES "public"."forms"("id") ON DELETE set null ON UPDATE no action;
CREATE INDEX "pages_blocks_fd_contact_form_form_idx" ON "pages_blocks_fd_contact_form" USING btree ("form_id");
CREATE INDEX "_pages_v_blocks_fd_contact_form_form_idx" ON "_pages_v_blocks_fd_contact_form" USING btree ("form_id");
ALTER TABLE "pages_blocks_fd_contact_form" DROP COLUMN "submit_endpoint";
ALTER TABLE "_pages_v_blocks_fd_contact_form" DROP COLUMN "submit_endpoint";
DROP TYPE "public"."enum_pages_blocks_fd_contact_form_form_fields_type";
DROP TYPE "public"."enum_pages_blocks_fd_contact_form_form_fields_width";
DROP TYPE "public"."enum__pages_v_blocks_fd_contact_form_form_fields_type";
DROP TYPE "public"."enum__pages_v_blocks_fd_contact_form_form_fields_width";`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
CREATE TYPE "public"."enum_pages_blocks_fd_contact_form_form_fields_type" AS ENUM('text', 'email', 'tel', 'textarea', 'select');
CREATE TYPE "public"."enum_pages_blocks_fd_contact_form_form_fields_width" AS ENUM('half', 'full');
CREATE TYPE "public"."enum__pages_v_blocks_fd_contact_form_form_fields_type" AS ENUM('text', 'email', 'tel', 'textarea', 'select');
CREATE TYPE "public"."enum__pages_v_blocks_fd_contact_form_form_fields_width" AS ENUM('half', 'full');
CREATE TABLE "pages_blocks_fd_contact_form_form_fields" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"label" varchar,
"name" varchar,
"type" "enum_pages_blocks_fd_contact_form_form_fields_type" DEFAULT 'text',
"required" boolean DEFAULT false,
"width" "enum_pages_blocks_fd_contact_form_form_fields_width" DEFAULT 'half',
"select_options" varchar
);
CREATE TABLE "_pages_v_blocks_fd_contact_form_form_fields" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"label" varchar,
"name" varchar,
"type" "enum__pages_v_blocks_fd_contact_form_form_fields_type" DEFAULT 'text',
"required" boolean DEFAULT false,
"width" "enum__pages_v_blocks_fd_contact_form_form_fields_width" DEFAULT 'half',
"select_options" varchar,
"_uuid" varchar
);
ALTER TABLE "pages_blocks_fd_contact_form" DROP CONSTRAINT "pages_blocks_fd_contact_form_form_id_forms_id_fk";
ALTER TABLE "_pages_v_blocks_fd_contact_form" DROP CONSTRAINT "_pages_v_blocks_fd_contact_form_form_id_forms_id_fk";
DROP INDEX "pages_blocks_fd_contact_form_form_idx";
DROP INDEX "_pages_v_blocks_fd_contact_form_form_idx";
ALTER TABLE "pages_blocks_fd_contact_form" ADD COLUMN "submit_endpoint" varchar;
ALTER TABLE "_pages_v_blocks_fd_contact_form" ADD COLUMN "submit_endpoint" varchar;
ALTER TABLE "pages_blocks_fd_contact_form_form_fields" ADD CONSTRAINT "pages_blocks_fd_contact_form_form_fields_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages_blocks_fd_contact_form"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_pages_v_blocks_fd_contact_form_form_fields" ADD CONSTRAINT "_pages_v_blocks_fd_contact_form_form_fields_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_pages_v_blocks_fd_contact_form"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "pages_blocks_fd_contact_form_form_fields_order_idx" ON "pages_blocks_fd_contact_form_form_fields" USING btree ("_order");
CREATE INDEX "pages_blocks_fd_contact_form_form_fields_parent_id_idx" ON "pages_blocks_fd_contact_form_form_fields" USING btree ("_parent_id");
CREATE INDEX "_pages_v_blocks_fd_contact_form_form_fields_order_idx" ON "_pages_v_blocks_fd_contact_form_form_fields" USING btree ("_order");
CREATE INDEX "_pages_v_blocks_fd_contact_form_form_fields_parent_id_idx" ON "_pages_v_blocks_fd_contact_form_form_fields" USING btree ("_parent_id");
ALTER TABLE "pages_blocks_fd_contact_form" DROP COLUMN "form_id";
ALTER TABLE "_pages_v_blocks_fd_contact_form" DROP COLUMN "form_id";`)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
// Migration code
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
// Migration code
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
// Migration code
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
// Migration code
}

File diff suppressed because it is too large Load Diff

View File

@ -1,635 +0,0 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TYPE "public"."enum_pages_blocks_fd_spacer_section_background" AS ENUM('white', 'navy', 'gray', 'yellow', 'transparent');
CREATE TYPE "public"."enum_pages_blocks_fd_icon_bar_section_background" AS ENUM('white', 'gray', 'navy', 'yellow');
CREATE TYPE "public"."enum_pages_blocks_fd_usp_checklist_section_background" AS ENUM('white', 'gray', 'navy');
CREATE TYPE "public"."enum_pages_blocks_fd_tech_properties_section_background" AS ENUM('navy', 'white', 'gray', 'yellow');
CREATE TYPE "public"."enum_pages_blocks_fd_usp_table_section_background" AS ENUM('white', 'gray', 'navy');
CREATE TYPE "public"."enum_pages_blocks_fd_header_text_image_section_background" AS ENUM('white', 'gray', 'navy');
CREATE TYPE "public"."enum_pages_blocks_fd_contact_form_section_background" AS ENUM('white', 'gray', 'navy', 'navyGradient');
CREATE TYPE "public"."enum_pages_blocks_fd_contact_form_layout" AS ENUM('standard', 'withImage', 'card');
CREATE TYPE "public"."enum_pages_blocks_fd_text_section_background" AS ENUM('white', 'navy', 'gray', 'yellow');
CREATE TYPE "public"."enum_pages_blocks_fd_cta_banner_section_background" AS ENUM('yellow', 'navy', 'gray', 'white');
CREATE TYPE "public"."enum_pages_blocks_fd_cta_banner_alignment" AS ENUM('center', 'left');
CREATE TYPE "public"."enum_pages_blocks_fd_cta_banner_size" AS ENUM('small', 'medium', 'large');
CREATE TYPE "public"."enum_pages_blocks_fd_testimonial_layout" AS ENUM('grid', 'featured');
CREATE TYPE "public"."enum_pages_blocks_fd_testimonial_section_background" AS ENUM('gray', 'white', 'navy');
CREATE TYPE "public"."enum_pages_blocks_fd_team_columns" AS ENUM('2', '3', '4');
CREATE TYPE "public"."enum_pages_blocks_fd_team_card_style" AS ENUM('navy', 'white', 'gray');
CREATE TYPE "public"."enum_pages_blocks_fd_team_section_background" AS ENUM('white', 'gray', 'navy');
CREATE TYPE "public"."enum__pages_v_blocks_fd_spacer_section_background" AS ENUM('white', 'navy', 'gray', 'yellow', 'transparent');
CREATE TYPE "public"."enum__pages_v_blocks_fd_icon_bar_section_background" AS ENUM('white', 'gray', 'navy', 'yellow');
CREATE TYPE "public"."enum__pages_v_blocks_fd_usp_checklist_section_background" AS ENUM('white', 'gray', 'navy');
CREATE TYPE "public"."enum__pages_v_blocks_fd_tech_properties_section_background" AS ENUM('navy', 'white', 'gray', 'yellow');
CREATE TYPE "public"."enum__pages_v_blocks_fd_usp_table_section_background" AS ENUM('white', 'gray', 'navy');
CREATE TYPE "public"."enum__pages_v_blocks_fd_header_text_image_section_background" AS ENUM('white', 'gray', 'navy');
CREATE TYPE "public"."enum__pages_v_blocks_fd_contact_form_section_background" AS ENUM('white', 'gray', 'navy', 'navyGradient');
CREATE TYPE "public"."enum__pages_v_blocks_fd_contact_form_layout" AS ENUM('standard', 'withImage', 'card');
CREATE TYPE "public"."enum__pages_v_blocks_fd_text_section_background" AS ENUM('white', 'navy', 'gray', 'yellow');
CREATE TYPE "public"."enum__pages_v_blocks_fd_cta_banner_section_background" AS ENUM('yellow', 'navy', 'gray', 'white');
CREATE TYPE "public"."enum__pages_v_blocks_fd_cta_banner_alignment" AS ENUM('center', 'left');
CREATE TYPE "public"."enum__pages_v_blocks_fd_cta_banner_size" AS ENUM('small', 'medium', 'large');
CREATE TYPE "public"."enum__pages_v_blocks_fd_testimonial_layout" AS ENUM('grid', 'featured');
CREATE TYPE "public"."enum__pages_v_blocks_fd_testimonial_section_background" AS ENUM('gray', 'white', 'navy');
CREATE TYPE "public"."enum__pages_v_blocks_fd_team_columns" AS ENUM('2', '3', '4');
CREATE TYPE "public"."enum__pages_v_blocks_fd_team_card_style" AS ENUM('navy', 'white', 'gray');
CREATE TYPE "public"."enum__pages_v_blocks_fd_team_section_background" AS ENUM('white', 'gray', 'navy');
CREATE TYPE "public"."enum_site_settings_cookie_consent_accepted_days" AS ENUM('0', '30', '90', '180', '365');
CREATE TYPE "public"."enum_site_settings_cookie_consent_declined_days" AS ENUM('0', '7', '14', '30', '90');
CREATE TABLE "pages_blocks_fd_cta_banner" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'Redo att komma igång?',
"subheading" varchar,
"cta_text" varchar DEFAULT 'Kontakta oss',
"cta_link" varchar DEFAULT '/kontakt',
"secondary_cta_text" varchar,
"secondary_cta_link" varchar,
"section_background" "enum_pages_blocks_fd_cta_banner_section_background" DEFAULT 'yellow',
"alignment" "enum_pages_blocks_fd_cta_banner_alignment" DEFAULT 'center',
"size" "enum_pages_blocks_fd_cta_banner_size" DEFAULT 'medium',
"block_name" varchar
);
CREATE TABLE "pages_blocks_fd_testimonial_testimonials" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"quote" varchar,
"author_name" varchar,
"author_role" varchar,
"author_company" varchar,
"avatar_id" integer
);
CREATE TABLE "pages_blocks_fd_testimonial" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"heading" varchar,
"layout" "enum_pages_blocks_fd_testimonial_layout" DEFAULT 'grid',
"section_background" "enum_pages_blocks_fd_testimonial_section_background" DEFAULT 'gray',
"block_name" varchar
);
CREATE TABLE "pages_blocks_fd_team_members" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"photo_id" integer,
"name" varchar,
"role" varchar,
"bio" varchar,
"email" varchar,
"linkedin" varchar
);
CREATE TABLE "pages_blocks_fd_team" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"heading" varchar,
"subheading" varchar,
"columns" "enum_pages_blocks_fd_team_columns" DEFAULT '3',
"card_style" "enum_pages_blocks_fd_team_card_style" DEFAULT 'navy',
"section_background" "enum_pages_blocks_fd_team_section_background" DEFAULT 'white',
"block_name" varchar
);
CREATE TABLE "_pages_v_blocks_fd_cta_banner" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'Redo att komma igång?',
"subheading" varchar,
"cta_text" varchar DEFAULT 'Kontakta oss',
"cta_link" varchar DEFAULT '/kontakt',
"secondary_cta_text" varchar,
"secondary_cta_link" varchar,
"section_background" "enum__pages_v_blocks_fd_cta_banner_section_background" DEFAULT 'yellow',
"alignment" "enum__pages_v_blocks_fd_cta_banner_alignment" DEFAULT 'center',
"size" "enum__pages_v_blocks_fd_cta_banner_size" DEFAULT 'medium',
"_uuid" varchar,
"block_name" varchar
);
CREATE TABLE "_pages_v_blocks_fd_testimonial_testimonials" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"quote" varchar,
"author_name" varchar,
"author_role" varchar,
"author_company" varchar,
"avatar_id" integer,
"_uuid" varchar
);
CREATE TABLE "_pages_v_blocks_fd_testimonial" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"heading" varchar,
"layout" "enum__pages_v_blocks_fd_testimonial_layout" DEFAULT 'grid',
"section_background" "enum__pages_v_blocks_fd_testimonial_section_background" DEFAULT 'gray',
"_uuid" varchar,
"block_name" varchar
);
CREATE TABLE "_pages_v_blocks_fd_team_members" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"photo_id" integer,
"name" varchar,
"role" varchar,
"bio" varchar,
"email" varchar,
"linkedin" varchar,
"_uuid" varchar
);
CREATE TABLE "_pages_v_blocks_fd_team" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"heading" varchar,
"subheading" varchar,
"columns" "enum__pages_v_blocks_fd_team_columns" DEFAULT '3',
"card_style" "enum__pages_v_blocks_fd_team_card_style" DEFAULT 'navy',
"section_background" "enum__pages_v_blocks_fd_team_section_background" DEFAULT 'white',
"_uuid" varchar,
"block_name" varchar
);
ALTER TABLE "pages_hero_links" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_rels" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "_pages_v_version_hero_links" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "_pages_v_rels" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "search_categories" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "search" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "search_rels" DISABLE ROW LEVEL SECURITY;
DROP TABLE "pages_hero_links" CASCADE;
DROP TABLE "pages_rels" CASCADE;
DROP TABLE "_pages_v_version_hero_links" CASCADE;
DROP TABLE "_pages_v_rels" CASCADE;
DROP TABLE "search_categories" CASCADE;
DROP TABLE "search" CASCADE;
DROP TABLE "search_rels" CASCADE;
ALTER TABLE "pages" DROP CONSTRAINT "pages_hero_media_id_media_id_fk";
ALTER TABLE "_pages_v" DROP CONSTRAINT "_pages_v_version_hero_media_id_media_id_fk";
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_search_fk";
DROP INDEX "pages_hero_hero_media_idx";
DROP INDEX "_pages_v_version_hero_version_hero_media_idx";
DROP INDEX "payload_locked_documents_rels_search_id_idx";
DROP INDEX "pages_slug_idx";
DROP INDEX "posts_slug_idx";
ALTER TABLE "pages_blocks_fd_faq_items" ALTER COLUMN "answer" SET DATA TYPE jsonb;
ALTER TABLE "pages_blocks_fd_usp_table_rows" ALTER COLUMN "description" SET DATA TYPE jsonb;
ALTER TABLE "pages_blocks_fd_contact_form" ALTER COLUMN "description" SET DEFAULT 'Berätta om era mål — vårt team kontaktar er och hjälper er hitta rätt lösning.';
ALTER TABLE "pages_blocks_fd_text" ALTER COLUMN "body" SET DATA TYPE jsonb;
ALTER TABLE "_pages_v_blocks_fd_faq_items" ALTER COLUMN "answer" SET DATA TYPE jsonb;
ALTER TABLE "_pages_v_blocks_fd_usp_table_rows" ALTER COLUMN "description" SET DATA TYPE jsonb;
ALTER TABLE "_pages_v_blocks_fd_contact_form" ALTER COLUMN "description" SET DEFAULT 'Berätta om era mål — vårt team kontaktar er och hjälper er hitta rätt lösning.';
ALTER TABLE "_pages_v_blocks_fd_text" ALTER COLUMN "body" SET DATA TYPE jsonb;
ALTER TABLE "pages_blocks_fd_spacer" ADD COLUMN "section_background" "enum_pages_blocks_fd_spacer_section_background" DEFAULT 'white';
ALTER TABLE "pages_blocks_fd_icon_bar" ADD COLUMN "section_background" "enum_pages_blocks_fd_icon_bar_section_background" DEFAULT 'gray';
ALTER TABLE "pages_blocks_fd_usp_checklist" ADD COLUMN "section_background" "enum_pages_blocks_fd_usp_checklist_section_background" DEFAULT 'white';
ALTER TABLE "pages_blocks_fd_tech_properties" ADD COLUMN "section_background" "enum_pages_blocks_fd_tech_properties_section_background" DEFAULT 'navy';
ALTER TABLE "pages_blocks_fd_usp_table" ADD COLUMN "section_background" "enum_pages_blocks_fd_usp_table_section_background" DEFAULT 'white';
ALTER TABLE "pages_blocks_fd_header_text_image" ADD COLUMN "section_background" "enum_pages_blocks_fd_header_text_image_section_background" DEFAULT 'white';
ALTER TABLE "pages_blocks_fd_contact_form" ADD COLUMN "section_background" "enum_pages_blocks_fd_contact_form_section_background" DEFAULT 'white';
ALTER TABLE "pages_blocks_fd_contact_form" ADD COLUMN "layout" "enum_pages_blocks_fd_contact_form_layout" DEFAULT 'standard';
ALTER TABLE "pages_blocks_fd_contact_form" ADD COLUMN "external_api_enabled" boolean DEFAULT false;
ALTER TABLE "pages_blocks_fd_contact_form" ADD COLUMN "external_api_endpoint" varchar;
ALTER TABLE "pages_blocks_fd_contact_form" ADD COLUMN "external_api_auth_token" varchar;
ALTER TABLE "pages_blocks_fd_locations_grid" ADD COLUMN "heading" varchar;
ALTER TABLE "pages_blocks_fd_alternate_hero" ADD COLUMN "heading" varchar;
ALTER TABLE "pages_blocks_fd_statistics" ADD COLUMN "heading" varchar;
ALTER TABLE "pages_blocks_fd_partners_logos" ADD COLUMN "heading" varchar DEFAULT 'Våra partners';
ALTER TABLE "pages_blocks_fd_service_chooser" ADD COLUMN "heading" varchar DEFAULT 'Välj din bransch';
ALTER TABLE "pages_blocks_fd_data_table" ADD COLUMN "heading" varchar;
ALTER TABLE "pages_blocks_fd_text" ADD COLUMN "section_background" "enum_pages_blocks_fd_text_section_background" DEFAULT 'white';
ALTER TABLE "_pages_v_blocks_fd_spacer" ADD COLUMN "section_background" "enum__pages_v_blocks_fd_spacer_section_background" DEFAULT 'white';
ALTER TABLE "_pages_v_blocks_fd_icon_bar" ADD COLUMN "section_background" "enum__pages_v_blocks_fd_icon_bar_section_background" DEFAULT 'gray';
ALTER TABLE "_pages_v_blocks_fd_usp_checklist" ADD COLUMN "section_background" "enum__pages_v_blocks_fd_usp_checklist_section_background" DEFAULT 'white';
ALTER TABLE "_pages_v_blocks_fd_tech_properties" ADD COLUMN "section_background" "enum__pages_v_blocks_fd_tech_properties_section_background" DEFAULT 'navy';
ALTER TABLE "_pages_v_blocks_fd_usp_table" ADD COLUMN "section_background" "enum__pages_v_blocks_fd_usp_table_section_background" DEFAULT 'white';
ALTER TABLE "_pages_v_blocks_fd_header_text_image" ADD COLUMN "section_background" "enum__pages_v_blocks_fd_header_text_image_section_background" DEFAULT 'white';
ALTER TABLE "_pages_v_blocks_fd_contact_form" ADD COLUMN "section_background" "enum__pages_v_blocks_fd_contact_form_section_background" DEFAULT 'white';
ALTER TABLE "_pages_v_blocks_fd_contact_form" ADD COLUMN "layout" "enum__pages_v_blocks_fd_contact_form_layout" DEFAULT 'standard';
ALTER TABLE "_pages_v_blocks_fd_contact_form" ADD COLUMN "external_api_enabled" boolean DEFAULT false;
ALTER TABLE "_pages_v_blocks_fd_contact_form" ADD COLUMN "external_api_endpoint" varchar;
ALTER TABLE "_pages_v_blocks_fd_contact_form" ADD COLUMN "external_api_auth_token" varchar;
ALTER TABLE "_pages_v_blocks_fd_locations_grid" ADD COLUMN "heading" varchar;
ALTER TABLE "_pages_v_blocks_fd_alternate_hero" ADD COLUMN "heading" varchar;
ALTER TABLE "_pages_v_blocks_fd_statistics" ADD COLUMN "heading" varchar;
ALTER TABLE "_pages_v_blocks_fd_partners_logos" ADD COLUMN "heading" varchar DEFAULT 'Våra partners';
ALTER TABLE "_pages_v_blocks_fd_service_chooser" ADD COLUMN "heading" varchar DEFAULT 'Välj din bransch';
ALTER TABLE "_pages_v_blocks_fd_data_table" ADD COLUMN "heading" varchar;
ALTER TABLE "_pages_v_blocks_fd_text" ADD COLUMN "section_background" "enum__pages_v_blocks_fd_text_section_background" DEFAULT 'white';
ALTER TABLE "site_settings" ADD COLUMN "cookie_consent_accepted_days" "enum_site_settings_cookie_consent_accepted_days" DEFAULT '365';
ALTER TABLE "site_settings" ADD COLUMN "cookie_consent_declined_days" "enum_site_settings_cookie_consent_declined_days" DEFAULT '30';
ALTER TABLE "pages_blocks_fd_cta_banner" ADD CONSTRAINT "pages_blocks_fd_cta_banner_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_fd_testimonial_testimonials" ADD CONSTRAINT "pages_blocks_fd_testimonial_testimonials_avatar_id_media_id_fk" FOREIGN KEY ("avatar_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "pages_blocks_fd_testimonial_testimonials" ADD CONSTRAINT "pages_blocks_fd_testimonial_testimonials_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages_blocks_fd_testimonial"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_fd_testimonial" ADD CONSTRAINT "pages_blocks_fd_testimonial_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_fd_team_members" ADD CONSTRAINT "pages_blocks_fd_team_members_photo_id_media_id_fk" FOREIGN KEY ("photo_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "pages_blocks_fd_team_members" ADD CONSTRAINT "pages_blocks_fd_team_members_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages_blocks_fd_team"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_fd_team" ADD CONSTRAINT "pages_blocks_fd_team_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_pages_v_blocks_fd_cta_banner" ADD CONSTRAINT "_pages_v_blocks_fd_cta_banner_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_pages_v"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_pages_v_blocks_fd_testimonial_testimonials" ADD CONSTRAINT "_pages_v_blocks_fd_testimonial_testimonials_avatar_id_media_id_fk" FOREIGN KEY ("avatar_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "_pages_v_blocks_fd_testimonial_testimonials" ADD CONSTRAINT "_pages_v_blocks_fd_testimonial_testimonials_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_pages_v_blocks_fd_testimonial"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_pages_v_blocks_fd_testimonial" ADD CONSTRAINT "_pages_v_blocks_fd_testimonial_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_pages_v"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_pages_v_blocks_fd_team_members" ADD CONSTRAINT "_pages_v_blocks_fd_team_members_photo_id_media_id_fk" FOREIGN KEY ("photo_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "_pages_v_blocks_fd_team_members" ADD CONSTRAINT "_pages_v_blocks_fd_team_members_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_pages_v_blocks_fd_team"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_pages_v_blocks_fd_team" ADD CONSTRAINT "_pages_v_blocks_fd_team_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_pages_v"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "pages_blocks_fd_cta_banner_order_idx" ON "pages_blocks_fd_cta_banner" USING btree ("_order");
CREATE INDEX "pages_blocks_fd_cta_banner_parent_id_idx" ON "pages_blocks_fd_cta_banner" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_fd_cta_banner_path_idx" ON "pages_blocks_fd_cta_banner" USING btree ("_path");
CREATE INDEX "pages_blocks_fd_testimonial_testimonials_order_idx" ON "pages_blocks_fd_testimonial_testimonials" USING btree ("_order");
CREATE INDEX "pages_blocks_fd_testimonial_testimonials_parent_id_idx" ON "pages_blocks_fd_testimonial_testimonials" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_fd_testimonial_testimonials_avatar_idx" ON "pages_blocks_fd_testimonial_testimonials" USING btree ("avatar_id");
CREATE INDEX "pages_blocks_fd_testimonial_order_idx" ON "pages_blocks_fd_testimonial" USING btree ("_order");
CREATE INDEX "pages_blocks_fd_testimonial_parent_id_idx" ON "pages_blocks_fd_testimonial" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_fd_testimonial_path_idx" ON "pages_blocks_fd_testimonial" USING btree ("_path");
CREATE INDEX "pages_blocks_fd_team_members_order_idx" ON "pages_blocks_fd_team_members" USING btree ("_order");
CREATE INDEX "pages_blocks_fd_team_members_parent_id_idx" ON "pages_blocks_fd_team_members" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_fd_team_members_photo_idx" ON "pages_blocks_fd_team_members" USING btree ("photo_id");
CREATE INDEX "pages_blocks_fd_team_order_idx" ON "pages_blocks_fd_team" USING btree ("_order");
CREATE INDEX "pages_blocks_fd_team_parent_id_idx" ON "pages_blocks_fd_team" USING btree ("_parent_id");
CREATE INDEX "pages_blocks_fd_team_path_idx" ON "pages_blocks_fd_team" USING btree ("_path");
CREATE INDEX "_pages_v_blocks_fd_cta_banner_order_idx" ON "_pages_v_blocks_fd_cta_banner" USING btree ("_order");
CREATE INDEX "_pages_v_blocks_fd_cta_banner_parent_id_idx" ON "_pages_v_blocks_fd_cta_banner" USING btree ("_parent_id");
CREATE INDEX "_pages_v_blocks_fd_cta_banner_path_idx" ON "_pages_v_blocks_fd_cta_banner" USING btree ("_path");
CREATE INDEX "_pages_v_blocks_fd_testimonial_testimonials_order_idx" ON "_pages_v_blocks_fd_testimonial_testimonials" USING btree ("_order");
CREATE INDEX "_pages_v_blocks_fd_testimonial_testimonials_parent_id_idx" ON "_pages_v_blocks_fd_testimonial_testimonials" USING btree ("_parent_id");
CREATE INDEX "_pages_v_blocks_fd_testimonial_testimonials_avatar_idx" ON "_pages_v_blocks_fd_testimonial_testimonials" USING btree ("avatar_id");
CREATE INDEX "_pages_v_blocks_fd_testimonial_order_idx" ON "_pages_v_blocks_fd_testimonial" USING btree ("_order");
CREATE INDEX "_pages_v_blocks_fd_testimonial_parent_id_idx" ON "_pages_v_blocks_fd_testimonial" USING btree ("_parent_id");
CREATE INDEX "_pages_v_blocks_fd_testimonial_path_idx" ON "_pages_v_blocks_fd_testimonial" USING btree ("_path");
CREATE INDEX "_pages_v_blocks_fd_team_members_order_idx" ON "_pages_v_blocks_fd_team_members" USING btree ("_order");
CREATE INDEX "_pages_v_blocks_fd_team_members_parent_id_idx" ON "_pages_v_blocks_fd_team_members" USING btree ("_parent_id");
CREATE INDEX "_pages_v_blocks_fd_team_members_photo_idx" ON "_pages_v_blocks_fd_team_members" USING btree ("photo_id");
CREATE INDEX "_pages_v_blocks_fd_team_order_idx" ON "_pages_v_blocks_fd_team" USING btree ("_order");
CREATE INDEX "_pages_v_blocks_fd_team_parent_id_idx" ON "_pages_v_blocks_fd_team" USING btree ("_parent_id");
CREATE INDEX "_pages_v_blocks_fd_team_path_idx" ON "_pages_v_blocks_fd_team" USING btree ("_path");
CREATE INDEX "pages_slug_idx" ON "pages" USING btree ("slug");
CREATE INDEX "posts_slug_idx" ON "posts" USING btree ("slug");
ALTER TABLE "pages_blocks_fd_spacer" DROP COLUMN "background";
ALTER TABLE "pages_blocks_fd_icon_bar" DROP COLUMN "background";
ALTER TABLE "pages_blocks_fd_usp_checklist" DROP COLUMN "background";
ALTER TABLE "pages_blocks_fd_tech_properties" DROP COLUMN "background";
ALTER TABLE "pages_blocks_fd_usp_table" DROP COLUMN "background";
ALTER TABLE "pages_blocks_fd_header_text_image" DROP COLUMN "background";
ALTER TABLE "pages_blocks_fd_contact_form" DROP COLUMN "background";
ALTER TABLE "pages_blocks_fd_locations_grid" DROP COLUMN "title";
ALTER TABLE "pages_blocks_fd_alternate_hero" DROP COLUMN "title";
ALTER TABLE "pages_blocks_fd_statistics" DROP COLUMN "title";
ALTER TABLE "pages_blocks_fd_partners_logos" DROP COLUMN "title";
ALTER TABLE "pages_blocks_fd_service_chooser" DROP COLUMN "title";
ALTER TABLE "pages_blocks_fd_data_table" DROP COLUMN "title";
ALTER TABLE "pages_blocks_fd_text" DROP COLUMN "background";
ALTER TABLE "pages" DROP COLUMN "hero_type";
ALTER TABLE "pages" DROP COLUMN "hero_rich_text";
ALTER TABLE "pages" DROP COLUMN "hero_media_id";
ALTER TABLE "pages" DROP COLUMN "generate_slug";
ALTER TABLE "_pages_v_blocks_fd_spacer" DROP COLUMN "background";
ALTER TABLE "_pages_v_blocks_fd_icon_bar" DROP COLUMN "background";
ALTER TABLE "_pages_v_blocks_fd_usp_checklist" DROP COLUMN "background";
ALTER TABLE "_pages_v_blocks_fd_tech_properties" DROP COLUMN "background";
ALTER TABLE "_pages_v_blocks_fd_usp_table" DROP COLUMN "background";
ALTER TABLE "_pages_v_blocks_fd_header_text_image" DROP COLUMN "background";
ALTER TABLE "_pages_v_blocks_fd_contact_form" DROP COLUMN "background";
ALTER TABLE "_pages_v_blocks_fd_locations_grid" DROP COLUMN "title";
ALTER TABLE "_pages_v_blocks_fd_alternate_hero" DROP COLUMN "title";
ALTER TABLE "_pages_v_blocks_fd_statistics" DROP COLUMN "title";
ALTER TABLE "_pages_v_blocks_fd_partners_logos" DROP COLUMN "title";
ALTER TABLE "_pages_v_blocks_fd_service_chooser" DROP COLUMN "title";
ALTER TABLE "_pages_v_blocks_fd_data_table" DROP COLUMN "title";
ALTER TABLE "_pages_v_blocks_fd_text" DROP COLUMN "background";
ALTER TABLE "_pages_v" DROP COLUMN "version_hero_type";
ALTER TABLE "_pages_v" DROP COLUMN "version_hero_rich_text";
ALTER TABLE "_pages_v" DROP COLUMN "version_hero_media_id";
ALTER TABLE "_pages_v" DROP COLUMN "version_generate_slug";
ALTER TABLE "posts" DROP COLUMN "generate_slug";
ALTER TABLE "_posts_v" DROP COLUMN "version_generate_slug";
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "search_id";
DROP TYPE "public"."enum_pages_hero_links_link_type";
DROP TYPE "public"."enum_pages_hero_links_link_appearance";
DROP TYPE "public"."enum_pages_blocks_fd_spacer_background";
DROP TYPE "public"."enum_pages_blocks_fd_icon_bar_background";
DROP TYPE "public"."enum_pages_blocks_fd_usp_checklist_background";
DROP TYPE "public"."enum_pages_blocks_fd_tech_properties_background";
DROP TYPE "public"."enum_pages_blocks_fd_usp_table_background";
DROP TYPE "public"."enum_pages_blocks_fd_header_text_image_background";
DROP TYPE "public"."enum_pages_blocks_fd_contact_form_background";
DROP TYPE "public"."enum_pages_blocks_fd_text_background";
DROP TYPE "public"."enum_pages_hero_type";
DROP TYPE "public"."enum__pages_v_version_hero_links_link_type";
DROP TYPE "public"."enum__pages_v_version_hero_links_link_appearance";
DROP TYPE "public"."enum__pages_v_blocks_fd_spacer_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_icon_bar_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_usp_checklist_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_tech_properties_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_usp_table_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_header_text_image_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_contact_form_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_text_background";
DROP TYPE "public"."enum__pages_v_version_hero_type";`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
CREATE TYPE "public"."enum_pages_hero_links_link_type" AS ENUM('reference', 'custom');
CREATE TYPE "public"."enum_pages_hero_links_link_appearance" AS ENUM('default', 'outline');
CREATE TYPE "public"."enum_pages_blocks_fd_spacer_background" AS ENUM('white', 'navy', 'gray', 'yellow', 'transparent');
CREATE TYPE "public"."enum_pages_blocks_fd_icon_bar_background" AS ENUM('white', 'gray', 'navy', 'yellow');
CREATE TYPE "public"."enum_pages_blocks_fd_usp_checklist_background" AS ENUM('white', 'gray', 'navy');
CREATE TYPE "public"."enum_pages_blocks_fd_tech_properties_background" AS ENUM('navy', 'white', 'gray', 'yellow');
CREATE TYPE "public"."enum_pages_blocks_fd_usp_table_background" AS ENUM('white', 'gray', 'navy');
CREATE TYPE "public"."enum_pages_blocks_fd_header_text_image_background" AS ENUM('white', 'gray', 'navy');
CREATE TYPE "public"."enum_pages_blocks_fd_contact_form_background" AS ENUM('white', 'gray');
CREATE TYPE "public"."enum_pages_blocks_fd_text_background" AS ENUM('white', 'navy', 'gray', 'yellow');
CREATE TYPE "public"."enum_pages_hero_type" AS ENUM('none', 'highImpact', 'mediumImpact', 'lowImpact');
CREATE TYPE "public"."enum__pages_v_version_hero_links_link_type" AS ENUM('reference', 'custom');
CREATE TYPE "public"."enum__pages_v_version_hero_links_link_appearance" AS ENUM('default', 'outline');
CREATE TYPE "public"."enum__pages_v_blocks_fd_spacer_background" AS ENUM('white', 'navy', 'gray', 'yellow', 'transparent');
CREATE TYPE "public"."enum__pages_v_blocks_fd_icon_bar_background" AS ENUM('white', 'gray', 'navy', 'yellow');
CREATE TYPE "public"."enum__pages_v_blocks_fd_usp_checklist_background" AS ENUM('white', 'gray', 'navy');
CREATE TYPE "public"."enum__pages_v_blocks_fd_tech_properties_background" AS ENUM('navy', 'white', 'gray', 'yellow');
CREATE TYPE "public"."enum__pages_v_blocks_fd_usp_table_background" AS ENUM('white', 'gray', 'navy');
CREATE TYPE "public"."enum__pages_v_blocks_fd_header_text_image_background" AS ENUM('white', 'gray', 'navy');
CREATE TYPE "public"."enum__pages_v_blocks_fd_contact_form_background" AS ENUM('white', 'gray');
CREATE TYPE "public"."enum__pages_v_blocks_fd_text_background" AS ENUM('white', 'navy', 'gray', 'yellow');
CREATE TYPE "public"."enum__pages_v_version_hero_type" AS ENUM('none', 'highImpact', 'mediumImpact', 'lowImpact');
CREATE TABLE "pages_hero_links" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"link_type" "enum_pages_hero_links_link_type" DEFAULT 'reference',
"link_new_tab" boolean,
"link_url" varchar,
"link_label" varchar,
"link_appearance" "enum_pages_hero_links_link_appearance" DEFAULT 'default'
);
CREATE TABLE "pages_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"pages_id" integer,
"posts_id" integer
);
CREATE TABLE "_pages_v_version_hero_links" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"link_type" "enum__pages_v_version_hero_links_link_type" DEFAULT 'reference',
"link_new_tab" boolean,
"link_url" varchar,
"link_label" varchar,
"link_appearance" "enum__pages_v_version_hero_links_link_appearance" DEFAULT 'default',
"_uuid" varchar
);
CREATE TABLE "_pages_v_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"pages_id" integer,
"posts_id" integer
);
CREATE TABLE "search_categories" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"relation_to" varchar,
"category_i_d" varchar,
"title" varchar
);
CREATE TABLE "search" (
"id" serial PRIMARY KEY NOT NULL,
"title" varchar,
"priority" numeric,
"slug" varchar,
"meta_title" varchar,
"meta_description" varchar,
"meta_image_id" integer,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "search_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"posts_id" integer
);
ALTER TABLE "pages_blocks_fd_cta_banner" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_fd_testimonial_testimonials" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_fd_testimonial" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_fd_team_members" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "pages_blocks_fd_team" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "_pages_v_blocks_fd_cta_banner" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "_pages_v_blocks_fd_testimonial_testimonials" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "_pages_v_blocks_fd_testimonial" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "_pages_v_blocks_fd_team_members" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "_pages_v_blocks_fd_team" DISABLE ROW LEVEL SECURITY;
DROP TABLE "pages_blocks_fd_cta_banner" CASCADE;
DROP TABLE "pages_blocks_fd_testimonial_testimonials" CASCADE;
DROP TABLE "pages_blocks_fd_testimonial" CASCADE;
DROP TABLE "pages_blocks_fd_team_members" CASCADE;
DROP TABLE "pages_blocks_fd_team" CASCADE;
DROP TABLE "_pages_v_blocks_fd_cta_banner" CASCADE;
DROP TABLE "_pages_v_blocks_fd_testimonial_testimonials" CASCADE;
DROP TABLE "_pages_v_blocks_fd_testimonial" CASCADE;
DROP TABLE "_pages_v_blocks_fd_team_members" CASCADE;
DROP TABLE "_pages_v_blocks_fd_team" CASCADE;
DROP INDEX "pages_slug_idx";
DROP INDEX "posts_slug_idx";
ALTER TABLE "pages_blocks_fd_faq_items" ALTER COLUMN "answer" SET DATA TYPE varchar;
ALTER TABLE "pages_blocks_fd_usp_table_rows" ALTER COLUMN "description" SET DATA TYPE varchar;
ALTER TABLE "pages_blocks_fd_contact_form" ALTER COLUMN "description" SET DEFAULT 'Tell us about your goals, our team will reach out to you and help you achieve the right solution.';
ALTER TABLE "pages_blocks_fd_text" ALTER COLUMN "body" SET DATA TYPE varchar;
ALTER TABLE "_pages_v_blocks_fd_faq_items" ALTER COLUMN "answer" SET DATA TYPE varchar;
ALTER TABLE "_pages_v_blocks_fd_usp_table_rows" ALTER COLUMN "description" SET DATA TYPE varchar;
ALTER TABLE "_pages_v_blocks_fd_contact_form" ALTER COLUMN "description" SET DEFAULT 'Tell us about your goals, our team will reach out to you and help you achieve the right solution.';
ALTER TABLE "_pages_v_blocks_fd_text" ALTER COLUMN "body" SET DATA TYPE varchar;
ALTER TABLE "pages_blocks_fd_spacer" ADD COLUMN "background" "enum_pages_blocks_fd_spacer_background" DEFAULT 'white';
ALTER TABLE "pages_blocks_fd_icon_bar" ADD COLUMN "background" "enum_pages_blocks_fd_icon_bar_background" DEFAULT 'gray';
ALTER TABLE "pages_blocks_fd_usp_checklist" ADD COLUMN "background" "enum_pages_blocks_fd_usp_checklist_background" DEFAULT 'white';
ALTER TABLE "pages_blocks_fd_tech_properties" ADD COLUMN "background" "enum_pages_blocks_fd_tech_properties_background" DEFAULT 'navy';
ALTER TABLE "pages_blocks_fd_usp_table" ADD COLUMN "background" "enum_pages_blocks_fd_usp_table_background" DEFAULT 'white';
ALTER TABLE "pages_blocks_fd_header_text_image" ADD COLUMN "background" "enum_pages_blocks_fd_header_text_image_background" DEFAULT 'white';
ALTER TABLE "pages_blocks_fd_contact_form" ADD COLUMN "background" "enum_pages_blocks_fd_contact_form_background" DEFAULT 'white';
ALTER TABLE "pages_blocks_fd_locations_grid" ADD COLUMN "title" varchar;
ALTER TABLE "pages_blocks_fd_alternate_hero" ADD COLUMN "title" varchar;
ALTER TABLE "pages_blocks_fd_statistics" ADD COLUMN "title" varchar;
ALTER TABLE "pages_blocks_fd_partners_logos" ADD COLUMN "title" varchar DEFAULT 'Våra partners';
ALTER TABLE "pages_blocks_fd_service_chooser" ADD COLUMN "title" varchar DEFAULT 'Välj din bransch';
ALTER TABLE "pages_blocks_fd_data_table" ADD COLUMN "title" varchar;
ALTER TABLE "pages_blocks_fd_text" ADD COLUMN "background" "enum_pages_blocks_fd_text_background" DEFAULT 'white';
ALTER TABLE "pages" ADD COLUMN "hero_type" "enum_pages_hero_type" DEFAULT 'lowImpact';
ALTER TABLE "pages" ADD COLUMN "hero_rich_text" jsonb;
ALTER TABLE "pages" ADD COLUMN "hero_media_id" integer;
ALTER TABLE "pages" ADD COLUMN "generate_slug" boolean DEFAULT true;
ALTER TABLE "_pages_v_blocks_fd_spacer" ADD COLUMN "background" "enum__pages_v_blocks_fd_spacer_background" DEFAULT 'white';
ALTER TABLE "_pages_v_blocks_fd_icon_bar" ADD COLUMN "background" "enum__pages_v_blocks_fd_icon_bar_background" DEFAULT 'gray';
ALTER TABLE "_pages_v_blocks_fd_usp_checklist" ADD COLUMN "background" "enum__pages_v_blocks_fd_usp_checklist_background" DEFAULT 'white';
ALTER TABLE "_pages_v_blocks_fd_tech_properties" ADD COLUMN "background" "enum__pages_v_blocks_fd_tech_properties_background" DEFAULT 'navy';
ALTER TABLE "_pages_v_blocks_fd_usp_table" ADD COLUMN "background" "enum__pages_v_blocks_fd_usp_table_background" DEFAULT 'white';
ALTER TABLE "_pages_v_blocks_fd_header_text_image" ADD COLUMN "background" "enum__pages_v_blocks_fd_header_text_image_background" DEFAULT 'white';
ALTER TABLE "_pages_v_blocks_fd_contact_form" ADD COLUMN "background" "enum__pages_v_blocks_fd_contact_form_background" DEFAULT 'white';
ALTER TABLE "_pages_v_blocks_fd_locations_grid" ADD COLUMN "title" varchar;
ALTER TABLE "_pages_v_blocks_fd_alternate_hero" ADD COLUMN "title" varchar;
ALTER TABLE "_pages_v_blocks_fd_statistics" ADD COLUMN "title" varchar;
ALTER TABLE "_pages_v_blocks_fd_partners_logos" ADD COLUMN "title" varchar DEFAULT 'Våra partners';
ALTER TABLE "_pages_v_blocks_fd_service_chooser" ADD COLUMN "title" varchar DEFAULT 'Välj din bransch';
ALTER TABLE "_pages_v_blocks_fd_data_table" ADD COLUMN "title" varchar;
ALTER TABLE "_pages_v_blocks_fd_text" ADD COLUMN "background" "enum__pages_v_blocks_fd_text_background" DEFAULT 'white';
ALTER TABLE "_pages_v" ADD COLUMN "version_hero_type" "enum__pages_v_version_hero_type" DEFAULT 'lowImpact';
ALTER TABLE "_pages_v" ADD COLUMN "version_hero_rich_text" jsonb;
ALTER TABLE "_pages_v" ADD COLUMN "version_hero_media_id" integer;
ALTER TABLE "_pages_v" ADD COLUMN "version_generate_slug" boolean DEFAULT true;
ALTER TABLE "posts" ADD COLUMN "generate_slug" boolean DEFAULT true;
ALTER TABLE "_posts_v" ADD COLUMN "version_generate_slug" boolean DEFAULT true;
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "search_id" integer;
ALTER TABLE "pages_hero_links" ADD CONSTRAINT "pages_hero_links_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_rels" ADD CONSTRAINT "pages_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_rels" ADD CONSTRAINT "pages_rels_pages_fk" FOREIGN KEY ("pages_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_rels" ADD CONSTRAINT "pages_rels_posts_fk" FOREIGN KEY ("posts_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_pages_v_version_hero_links" ADD CONSTRAINT "_pages_v_version_hero_links_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_pages_v"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_pages_v_rels" ADD CONSTRAINT "_pages_v_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."_pages_v"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_pages_v_rels" ADD CONSTRAINT "_pages_v_rels_pages_fk" FOREIGN KEY ("pages_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_pages_v_rels" ADD CONSTRAINT "_pages_v_rels_posts_fk" FOREIGN KEY ("posts_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "search_categories" ADD CONSTRAINT "search_categories_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."search"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "search" ADD CONSTRAINT "search_meta_image_id_media_id_fk" FOREIGN KEY ("meta_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "search_rels" ADD CONSTRAINT "search_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."search"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "search_rels" ADD CONSTRAINT "search_rels_posts_fk" FOREIGN KEY ("posts_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "pages_hero_links_order_idx" ON "pages_hero_links" USING btree ("_order");
CREATE INDEX "pages_hero_links_parent_id_idx" ON "pages_hero_links" USING btree ("_parent_id");
CREATE INDEX "pages_rels_order_idx" ON "pages_rels" USING btree ("order");
CREATE INDEX "pages_rels_parent_idx" ON "pages_rels" USING btree ("parent_id");
CREATE INDEX "pages_rels_path_idx" ON "pages_rels" USING btree ("path");
CREATE INDEX "pages_rels_pages_id_idx" ON "pages_rels" USING btree ("pages_id");
CREATE INDEX "pages_rels_posts_id_idx" ON "pages_rels" USING btree ("posts_id");
CREATE INDEX "_pages_v_version_hero_links_order_idx" ON "_pages_v_version_hero_links" USING btree ("_order");
CREATE INDEX "_pages_v_version_hero_links_parent_id_idx" ON "_pages_v_version_hero_links" USING btree ("_parent_id");
CREATE INDEX "_pages_v_rels_order_idx" ON "_pages_v_rels" USING btree ("order");
CREATE INDEX "_pages_v_rels_parent_idx" ON "_pages_v_rels" USING btree ("parent_id");
CREATE INDEX "_pages_v_rels_path_idx" ON "_pages_v_rels" USING btree ("path");
CREATE INDEX "_pages_v_rels_pages_id_idx" ON "_pages_v_rels" USING btree ("pages_id");
CREATE INDEX "_pages_v_rels_posts_id_idx" ON "_pages_v_rels" USING btree ("posts_id");
CREATE INDEX "search_categories_order_idx" ON "search_categories" USING btree ("_order");
CREATE INDEX "search_categories_parent_id_idx" ON "search_categories" USING btree ("_parent_id");
CREATE INDEX "search_slug_idx" ON "search" USING btree ("slug");
CREATE INDEX "search_meta_meta_image_idx" ON "search" USING btree ("meta_image_id");
CREATE INDEX "search_updated_at_idx" ON "search" USING btree ("updated_at");
CREATE INDEX "search_created_at_idx" ON "search" USING btree ("created_at");
CREATE INDEX "search_rels_order_idx" ON "search_rels" USING btree ("order");
CREATE INDEX "search_rels_parent_idx" ON "search_rels" USING btree ("parent_id");
CREATE INDEX "search_rels_path_idx" ON "search_rels" USING btree ("path");
CREATE INDEX "search_rels_posts_id_idx" ON "search_rels" USING btree ("posts_id");
ALTER TABLE "pages" ADD CONSTRAINT "pages_hero_media_id_media_id_fk" FOREIGN KEY ("hero_media_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "_pages_v" ADD CONSTRAINT "_pages_v_version_hero_media_id_media_id_fk" FOREIGN KEY ("version_hero_media_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_search_fk" FOREIGN KEY ("search_id") REFERENCES "public"."search"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "pages_hero_hero_media_idx" ON "pages" USING btree ("hero_media_id");
CREATE INDEX "_pages_v_version_hero_version_hero_media_idx" ON "_pages_v" USING btree ("version_hero_media_id");
CREATE INDEX "payload_locked_documents_rels_search_id_idx" ON "payload_locked_documents_rels" USING btree ("search_id");
CREATE UNIQUE INDEX "pages_slug_idx" ON "pages" USING btree ("slug");
CREATE UNIQUE INDEX "posts_slug_idx" ON "posts" USING btree ("slug");
ALTER TABLE "pages_blocks_fd_spacer" DROP COLUMN "section_background";
ALTER TABLE "pages_blocks_fd_icon_bar" DROP COLUMN "section_background";
ALTER TABLE "pages_blocks_fd_usp_checklist" DROP COLUMN "section_background";
ALTER TABLE "pages_blocks_fd_tech_properties" DROP COLUMN "section_background";
ALTER TABLE "pages_blocks_fd_usp_table" DROP COLUMN "section_background";
ALTER TABLE "pages_blocks_fd_header_text_image" DROP COLUMN "section_background";
ALTER TABLE "pages_blocks_fd_contact_form" DROP COLUMN "section_background";
ALTER TABLE "pages_blocks_fd_contact_form" DROP COLUMN "layout";
ALTER TABLE "pages_blocks_fd_contact_form" DROP COLUMN "external_api_enabled";
ALTER TABLE "pages_blocks_fd_contact_form" DROP COLUMN "external_api_endpoint";
ALTER TABLE "pages_blocks_fd_contact_form" DROP COLUMN "external_api_auth_token";
ALTER TABLE "pages_blocks_fd_locations_grid" DROP COLUMN "heading";
ALTER TABLE "pages_blocks_fd_alternate_hero" DROP COLUMN "heading";
ALTER TABLE "pages_blocks_fd_statistics" DROP COLUMN "heading";
ALTER TABLE "pages_blocks_fd_partners_logos" DROP COLUMN "heading";
ALTER TABLE "pages_blocks_fd_service_chooser" DROP COLUMN "heading";
ALTER TABLE "pages_blocks_fd_data_table" DROP COLUMN "heading";
ALTER TABLE "pages_blocks_fd_text" DROP COLUMN "section_background";
ALTER TABLE "_pages_v_blocks_fd_spacer" DROP COLUMN "section_background";
ALTER TABLE "_pages_v_blocks_fd_icon_bar" DROP COLUMN "section_background";
ALTER TABLE "_pages_v_blocks_fd_usp_checklist" DROP COLUMN "section_background";
ALTER TABLE "_pages_v_blocks_fd_tech_properties" DROP COLUMN "section_background";
ALTER TABLE "_pages_v_blocks_fd_usp_table" DROP COLUMN "section_background";
ALTER TABLE "_pages_v_blocks_fd_header_text_image" DROP COLUMN "section_background";
ALTER TABLE "_pages_v_blocks_fd_contact_form" DROP COLUMN "section_background";
ALTER TABLE "_pages_v_blocks_fd_contact_form" DROP COLUMN "layout";
ALTER TABLE "_pages_v_blocks_fd_contact_form" DROP COLUMN "external_api_enabled";
ALTER TABLE "_pages_v_blocks_fd_contact_form" DROP COLUMN "external_api_endpoint";
ALTER TABLE "_pages_v_blocks_fd_contact_form" DROP COLUMN "external_api_auth_token";
ALTER TABLE "_pages_v_blocks_fd_locations_grid" DROP COLUMN "heading";
ALTER TABLE "_pages_v_blocks_fd_alternate_hero" DROP COLUMN "heading";
ALTER TABLE "_pages_v_blocks_fd_statistics" DROP COLUMN "heading";
ALTER TABLE "_pages_v_blocks_fd_partners_logos" DROP COLUMN "heading";
ALTER TABLE "_pages_v_blocks_fd_service_chooser" DROP COLUMN "heading";
ALTER TABLE "_pages_v_blocks_fd_data_table" DROP COLUMN "heading";
ALTER TABLE "_pages_v_blocks_fd_text" DROP COLUMN "section_background";
ALTER TABLE "site_settings" DROP COLUMN "cookie_consent_accepted_days";
ALTER TABLE "site_settings" DROP COLUMN "cookie_consent_declined_days";
DROP TYPE "public"."enum_pages_blocks_fd_spacer_section_background";
DROP TYPE "public"."enum_pages_blocks_fd_icon_bar_section_background";
DROP TYPE "public"."enum_pages_blocks_fd_usp_checklist_section_background";
DROP TYPE "public"."enum_pages_blocks_fd_tech_properties_section_background";
DROP TYPE "public"."enum_pages_blocks_fd_usp_table_section_background";
DROP TYPE "public"."enum_pages_blocks_fd_header_text_image_section_background";
DROP TYPE "public"."enum_pages_blocks_fd_contact_form_section_background";
DROP TYPE "public"."enum_pages_blocks_fd_contact_form_layout";
DROP TYPE "public"."enum_pages_blocks_fd_text_section_background";
DROP TYPE "public"."enum_pages_blocks_fd_cta_banner_section_background";
DROP TYPE "public"."enum_pages_blocks_fd_cta_banner_alignment";
DROP TYPE "public"."enum_pages_blocks_fd_cta_banner_size";
DROP TYPE "public"."enum_pages_blocks_fd_testimonial_layout";
DROP TYPE "public"."enum_pages_blocks_fd_testimonial_section_background";
DROP TYPE "public"."enum_pages_blocks_fd_team_columns";
DROP TYPE "public"."enum_pages_blocks_fd_team_card_style";
DROP TYPE "public"."enum_pages_blocks_fd_team_section_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_spacer_section_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_icon_bar_section_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_usp_checklist_section_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_tech_properties_section_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_usp_table_section_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_header_text_image_section_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_contact_form_section_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_contact_form_layout";
DROP TYPE "public"."enum__pages_v_blocks_fd_text_section_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_cta_banner_section_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_cta_banner_alignment";
DROP TYPE "public"."enum__pages_v_blocks_fd_cta_banner_size";
DROP TYPE "public"."enum__pages_v_blocks_fd_testimonial_layout";
DROP TYPE "public"."enum__pages_v_blocks_fd_testimonial_section_background";
DROP TYPE "public"."enum__pages_v_blocks_fd_team_columns";
DROP TYPE "public"."enum__pages_v_blocks_fd_team_card_style";
DROP TYPE "public"."enum__pages_v_blocks_fd_team_section_background";
DROP TYPE "public"."enum_site_settings_cookie_consent_accepted_days";
DROP TYPE "public"."enum_site_settings_cookie_consent_declined_days";`)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +0,0 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TYPE "public"."enum_users_role" AS ENUM('admin', 'editor');
ALTER TABLE "users" ALTER COLUMN "name" SET NOT NULL;
ALTER TABLE "users" ADD COLUMN "role" "enum_users_role" DEFAULT 'editor' NOT NULL;`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "users" ALTER COLUMN "name" DROP NOT NULL;
ALTER TABLE "users" DROP COLUMN "role";
DROP TYPE "public"."enum_users_role";`)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +0,0 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TYPE "public"."enum_header_nav_items_children_type" AS ENUM('reference', 'custom');
CREATE TYPE "public"."enum_header_nav_items_type" AS ENUM('reference', 'custom');
CREATE TYPE "public"."enum_header_logo_link_type" AS ENUM('reference', 'custom');
CREATE TYPE "public"."enum_footer_logo_link_type" AS ENUM('reference', 'custom');
CREATE TABLE "header_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"pages_id" integer
);
ALTER TABLE "header_nav_items_children" ALTER COLUMN "url" DROP NOT NULL;
ALTER TABLE "pages" ADD COLUMN "generate_slug" boolean DEFAULT true;
ALTER TABLE "_pages_v" ADD COLUMN "version_generate_slug" boolean DEFAULT true;
ALTER TABLE "header_nav_items_children" ADD COLUMN "type" "enum_header_nav_items_children_type" DEFAULT 'custom';
ALTER TABLE "header_nav_items" ADD COLUMN "type" "enum_header_nav_items_type" DEFAULT 'custom';
ALTER TABLE "header" ADD COLUMN "logo_link_type" "enum_header_logo_link_type" DEFAULT 'custom';
ALTER TABLE "header" ADD COLUMN "logo_link_url" varchar DEFAULT '/';
ALTER TABLE "footer" ADD COLUMN "logo_link_type" "enum_footer_logo_link_type" DEFAULT 'custom';
ALTER TABLE "footer" ADD COLUMN "logo_link_url" varchar DEFAULT '/';
ALTER TABLE "header_rels" ADD CONSTRAINT "header_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."header"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "header_rels" ADD CONSTRAINT "header_rels_pages_fk" FOREIGN KEY ("pages_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "header_rels_order_idx" ON "header_rels" USING btree ("order");
CREATE INDEX "header_rels_parent_idx" ON "header_rels" USING btree ("parent_id");
CREATE INDEX "header_rels_path_idx" ON "header_rels" USING btree ("path");
CREATE INDEX "header_rels_pages_id_idx" ON "header_rels" USING btree ("pages_id");`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "header_rels" DISABLE ROW LEVEL SECURITY;
DROP TABLE "header_rels" CASCADE;
ALTER TABLE "header_nav_items_children" ALTER COLUMN "url" SET NOT NULL;
ALTER TABLE "pages" DROP COLUMN "generate_slug";
ALTER TABLE "_pages_v" DROP COLUMN "version_generate_slug";
ALTER TABLE "header_nav_items_children" DROP COLUMN "type";
ALTER TABLE "header_nav_items" DROP COLUMN "type";
ALTER TABLE "header" DROP COLUMN "logo_link_type";
ALTER TABLE "header" DROP COLUMN "logo_link_url";
ALTER TABLE "footer" DROP COLUMN "logo_link_type";
ALTER TABLE "footer" DROP COLUMN "logo_link_url";
DROP TYPE "public"."enum_header_nav_items_children_type";
DROP TYPE "public"."enum_header_nav_items_type";
DROP TYPE "public"."enum_header_logo_link_type";
DROP TYPE "public"."enum_footer_logo_link_type";`)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +0,0 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "pages_blocks_fd_cta_side_image" ALTER COLUMN "theme" SET DATA TYPE text;
ALTER TABLE "pages_blocks_fd_cta_side_image" ALTER COLUMN "theme" SET DEFAULT 'dark'::text;
DROP TYPE "public"."enum_pages_blocks_fd_cta_side_image_theme";
CREATE TYPE "public"."enum_pages_blocks_fd_cta_side_image_theme" AS ENUM('light', 'dark');
ALTER TABLE "pages_blocks_fd_cta_side_image" ALTER COLUMN "theme" SET DEFAULT 'dark'::"public"."enum_pages_blocks_fd_cta_side_image_theme";
ALTER TABLE "pages_blocks_fd_cta_side_image" ALTER COLUMN "theme" SET DATA TYPE "public"."enum_pages_blocks_fd_cta_side_image_theme" USING "theme"::"public"."enum_pages_blocks_fd_cta_side_image_theme";
ALTER TABLE "_pages_v_blocks_fd_cta_side_image" ALTER COLUMN "theme" SET DATA TYPE text;
ALTER TABLE "_pages_v_blocks_fd_cta_side_image" ALTER COLUMN "theme" SET DEFAULT 'dark'::text;
DROP TYPE "public"."enum__pages_v_blocks_fd_cta_side_image_theme";
CREATE TYPE "public"."enum__pages_v_blocks_fd_cta_side_image_theme" AS ENUM('light', 'dark');
ALTER TABLE "_pages_v_blocks_fd_cta_side_image" ALTER COLUMN "theme" SET DEFAULT 'dark'::"public"."enum__pages_v_blocks_fd_cta_side_image_theme";
ALTER TABLE "_pages_v_blocks_fd_cta_side_image" ALTER COLUMN "theme" SET DATA TYPE "public"."enum__pages_v_blocks_fd_cta_side_image_theme" USING "theme"::"public"."enum__pages_v_blocks_fd_cta_side_image_theme";
ALTER TABLE "pages_blocks_fd_cta_side_image" DROP COLUMN "custom_background_color";
ALTER TABLE "pages_blocks_fd_cta_side_image" DROP COLUMN "custom_text_light";
ALTER TABLE "pages_blocks_fd_cta_side_image" DROP COLUMN "image_overlay";
ALTER TABLE "pages_blocks_fd_cta_side_image" DROP COLUMN "image_overlay_opacity";
ALTER TABLE "_pages_v_blocks_fd_cta_side_image" DROP COLUMN "custom_background_color";
ALTER TABLE "_pages_v_blocks_fd_cta_side_image" DROP COLUMN "custom_text_light";
ALTER TABLE "_pages_v_blocks_fd_cta_side_image" DROP COLUMN "image_overlay";
ALTER TABLE "_pages_v_blocks_fd_cta_side_image" DROP COLUMN "image_overlay_opacity";
DROP TYPE "public"."enum_pages_blocks_fd_cta_side_image_image_overlay";
DROP TYPE "public"."enum_pages_blocks_fd_cta_side_image_image_overlay_opacity";
DROP TYPE "public"."enum__pages_v_blocks_fd_cta_side_image_image_overlay";
DROP TYPE "public"."enum__pages_v_blocks_fd_cta_side_image_image_overlay_opacity";`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
CREATE TYPE "public"."enum_pages_blocks_fd_cta_side_image_image_overlay" AS ENUM('none', 'navy', 'yellow', 'black');
CREATE TYPE "public"."enum_pages_blocks_fd_cta_side_image_image_overlay_opacity" AS ENUM('20', '30', '50', '70');
CREATE TYPE "public"."enum__pages_v_blocks_fd_cta_side_image_image_overlay" AS ENUM('none', 'navy', 'yellow', 'black');
CREATE TYPE "public"."enum__pages_v_blocks_fd_cta_side_image_image_overlay_opacity" AS ENUM('20', '30', '50', '70');
ALTER TYPE "public"."enum_pages_blocks_fd_cta_side_image_theme" ADD VALUE 'custom';
ALTER TYPE "public"."enum__pages_v_blocks_fd_cta_side_image_theme" ADD VALUE 'custom';
ALTER TABLE "pages_blocks_fd_cta_side_image" ADD COLUMN "custom_background_color" varchar;
ALTER TABLE "pages_blocks_fd_cta_side_image" ADD COLUMN "custom_text_light" boolean DEFAULT true;
ALTER TABLE "pages_blocks_fd_cta_side_image" ADD COLUMN "image_overlay" "enum_pages_blocks_fd_cta_side_image_image_overlay" DEFAULT 'none';
ALTER TABLE "pages_blocks_fd_cta_side_image" ADD COLUMN "image_overlay_opacity" "enum_pages_blocks_fd_cta_side_image_image_overlay_opacity" DEFAULT '30';
ALTER TABLE "_pages_v_blocks_fd_cta_side_image" ADD COLUMN "custom_background_color" varchar;
ALTER TABLE "_pages_v_blocks_fd_cta_side_image" ADD COLUMN "custom_text_light" boolean DEFAULT true;
ALTER TABLE "_pages_v_blocks_fd_cta_side_image" ADD COLUMN "image_overlay" "enum__pages_v_blocks_fd_cta_side_image_image_overlay" DEFAULT 'none';
ALTER TABLE "_pages_v_blocks_fd_cta_side_image" ADD COLUMN "image_overlay_opacity" "enum__pages_v_blocks_fd_cta_side_image_image_overlay_opacity" DEFAULT '30';`)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +0,0 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TABLE "footer_cert_marks" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"image_id" integer NOT NULL,
"alt" varchar,
"link_url" varchar
);
ALTER TABLE "footer" ADD COLUMN "social_links_linkedin_enabled" boolean DEFAULT false;
ALTER TABLE "footer" ADD COLUMN "social_links_linkedin_url" varchar;
ALTER TABLE "footer" ADD COLUMN "social_links_instagram_enabled" boolean DEFAULT false;
ALTER TABLE "footer" ADD COLUMN "social_links_instagram_url" varchar;
ALTER TABLE "footer" ADD COLUMN "social_links_facebook_enabled" boolean DEFAULT false;
ALTER TABLE "footer" ADD COLUMN "social_links_facebook_url" varchar;
ALTER TABLE "footer" ADD COLUMN "social_links_youtube_enabled" boolean DEFAULT false;
ALTER TABLE "footer" ADD COLUMN "social_links_youtube_url" varchar;
ALTER TABLE "footer" ADD COLUMN "social_links_twitter_enabled" boolean DEFAULT false;
ALTER TABLE "footer" ADD COLUMN "social_links_twitter_url" varchar;
ALTER TABLE "footer_cert_marks" ADD CONSTRAINT "footer_cert_marks_image_id_media_id_fk" FOREIGN KEY ("image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "footer_cert_marks" ADD CONSTRAINT "footer_cert_marks_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."footer"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "footer_cert_marks_order_idx" ON "footer_cert_marks" USING btree ("_order");
CREATE INDEX "footer_cert_marks_parent_id_idx" ON "footer_cert_marks" USING btree ("_parent_id");
CREATE INDEX "footer_cert_marks_image_idx" ON "footer_cert_marks" USING btree ("image_id");
ALTER TABLE "footer" DROP COLUMN "bottom_right_text";`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
DROP TABLE "footer_cert_marks" CASCADE;
ALTER TABLE "footer" ADD COLUMN "bottom_right_text" varchar DEFAULT 'Svenskt datacenter · ISO 27001 · ISO 14001';
ALTER TABLE "footer" DROP COLUMN "social_links_linkedin_enabled";
ALTER TABLE "footer" DROP COLUMN "social_links_linkedin_url";
ALTER TABLE "footer" DROP COLUMN "social_links_instagram_enabled";
ALTER TABLE "footer" DROP COLUMN "social_links_instagram_url";
ALTER TABLE "footer" DROP COLUMN "social_links_facebook_enabled";
ALTER TABLE "footer" DROP COLUMN "social_links_facebook_url";
ALTER TABLE "footer" DROP COLUMN "social_links_youtube_enabled";
ALTER TABLE "footer" DROP COLUMN "social_links_youtube_url";
ALTER TABLE "footer" DROP COLUMN "social_links_twitter_enabled";
ALTER TABLE "footer" DROP COLUMN "social_links_twitter_url";`)
}

View File

@ -1,75 +1 @@
import * as migration_20260216_151327 from './20260216_151327';
import * as migration_20260216_170021 from './20260216_170021';
import * as migration_20260216_171613 from './20260216_171613';
import * as migration_20260216_183152 from './20260216_183152';
import * as migration_20260216_184110 from './20260216_184110';
import * as migration_20260216_184342 from './20260216_184342';
import * as migration_20260218_130902 from './20260218_130902';
import * as migration_20260218_145924 from './20260218_145924';
import * as migration_20260219_175400 from './20260219_175400';
import * as migration_20260219_184333 from './20260219_184333';
import * as migration_20260219_194949 from './20260219_194949';
import * as migration_20260220_122722 from './20260220_122722';
export const migrations = [
{
up: migration_20260216_151327.up,
down: migration_20260216_151327.down,
name: '20260216_151327',
},
{
up: migration_20260216_170021.up,
down: migration_20260216_170021.down,
name: '20260216_170021',
},
{
up: migration_20260216_171613.up,
down: migration_20260216_171613.down,
name: '20260216_171613',
},
{
up: migration_20260216_183152.up,
down: migration_20260216_183152.down,
name: '20260216_183152',
},
{
up: migration_20260216_184110.up,
down: migration_20260216_184110.down,
name: '20260216_184110',
},
{
up: migration_20260216_184342.up,
down: migration_20260216_184342.down,
name: '20260216_184342',
},
{
up: migration_20260218_130902.up,
down: migration_20260218_130902.down,
name: '20260218_130902',
},
{
up: migration_20260218_145924.up,
down: migration_20260218_145924.down,
name: '20260218_145924',
},
{
up: migration_20260219_175400.up,
down: migration_20260219_175400.down,
name: '20260219_175400',
},
{
up: migration_20260219_184333.up,
down: migration_20260219_184333.down,
name: '20260219_184333',
},
{
up: migration_20260219_194949.up,
down: migration_20260219_194949.down,
name: '20260219_194949',
},
{
up: migration_20260220_122722.up,
down: migration_20260220_122722.down,
name: '20260220_122722'
},
];
export {}

View File

@ -178,6 +178,7 @@ export interface Page {
| FDServiceChooserBlock
| FDDataTableBlock
| FDVpsCalculatorBlock
| FDServiceCalculatorBlock
| FDTagsBlock
| FDTextBlock
| FDCodeEmbedBlock
@ -299,6 +300,9 @@ export interface FDCtaSideImageBlock {
ctaText?: string | null;
ctaLink?: string | null;
image?: (number | null) | Media;
imageOverlay?:
| ('none' | 'navyLight' | 'navyMedium' | 'yellowLight' | 'yellowMedium' | 'sepia' | 'blackLight' | 'blackMedium')
| null;
imagePosition?: ('right' | 'left') | null;
theme?: ('light' | 'dark') | null;
id?: string | null;
@ -1093,6 +1097,92 @@ export interface FDVpsCalculatorBlock {
blockName?: string | null;
blockType: 'fdVpsCalculator';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "FDServiceCalculatorBlock".
*/
export interface FDServiceCalculatorBlock {
heading?: string | null;
description?: string | null;
summaryHeading?: string | null;
totalLabel?: string | null;
totalSuffix?: string | null;
orderCtaText?: string | null;
orderCtaLink?: string | null;
contactCtaText?: string | null;
contactCtaLink?: string | null;
sectionBackground?: ('white' | 'gray' | 'navy') | null;
/**
* T.ex. "Operativsystem" med Linux/Windows, eller "Nivå" med Standard/Premium. Visas som knappar.
*/
optionGroups?:
| {
groupLabel: string;
options?:
| {
label: string;
/**
* 0 för gratis (t.ex. Linux)
*/
price?: number | null;
id?: string | null;
}[]
| null;
id?: string | null;
}[]
| null;
/**
* Varje rad blir ett +/- reglage. T.ex. CPU-kärnor, RAM (GB), Lagring (GB).
*/
resources?:
| {
label: string;
unit: string;
pricePerUnit: number;
defaultValue?: number | null;
min?: number | null;
max?: number | null;
step?: number | null;
/**
* Använd {value} och {unit} som variabler. T.ex. "SSD NVMe ({value} {unit})".
*/
summaryTemplate?: string | null;
id?: string | null;
}[]
| null;
/**
* Kunden kan slå /av dessa med en toggle.
*/
addons?:
| {
label: string;
price: number;
description?: string | null;
id?: string | null;
}[]
| null;
/**
* Alltid inkluderade i totalen. Kunden kan inte välja bort dessa.
*/
fixedFees?:
| {
label: string;
amount: number;
id?: string | null;
}[]
| null;
/**
* Appliceras alla resurser och tillval. 0 = ingen rabatt.
*/
discountPercent?: number | null;
/**
* T.ex. "{percent}% rabatt på alla resurser". Använd {percent} som variabel.
*/
discountLabel?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'fdServiceCalculator';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "FDTagsBlock".
@ -1679,6 +1769,7 @@ export interface PagesSelect<T extends boolean = true> {
fdServiceChooser?: T | FDServiceChooserBlockSelect<T>;
fdDataTable?: T | FDDataTableBlockSelect<T>;
fdVpsCalculator?: T | FDVpsCalculatorBlockSelect<T>;
fdServiceCalculator?: T | FDServiceCalculatorBlockSelect<T>;
fdTags?: T | FDTagsBlockSelect<T>;
fdText?: T | FDTextBlockSelect<T>;
fdCodeEmbed?: T | FDCodeEmbedBlockSelect<T>;
@ -1730,6 +1821,7 @@ export interface FDCtaSideImageBlockSelect<T extends boolean = true> {
ctaText?: T;
ctaLink?: T;
image?: T;
imageOverlay?: T;
imagePosition?: T;
theme?: T;
id?: T;
@ -2185,6 +2277,67 @@ export interface FDVpsCalculatorBlockSelect<T extends boolean = true> {
id?: T;
blockName?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "FDServiceCalculatorBlock_select".
*/
export interface FDServiceCalculatorBlockSelect<T extends boolean = true> {
heading?: T;
description?: T;
summaryHeading?: T;
totalLabel?: T;
totalSuffix?: T;
orderCtaText?: T;
orderCtaLink?: T;
contactCtaText?: T;
contactCtaLink?: T;
sectionBackground?: T;
optionGroups?:
| T
| {
groupLabel?: T;
options?:
| T
| {
label?: T;
price?: T;
id?: T;
};
id?: T;
};
resources?:
| T
| {
label?: T;
unit?: T;
pricePerUnit?: T;
defaultValue?: T;
min?: T;
max?: T;
step?: T;
summaryTemplate?: T;
id?: T;
};
addons?:
| T
| {
label?: T;
price?: T;
description?: T;
id?: T;
};
fixedFees?:
| T
| {
label?: T;
amount?: T;
id?: T;
};
discountPercent?: T;
discountLabel?: T;
id?: T;
blockName?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "FDTagsBlock_select".

76
src/utilities/fdTheme.ts Normal file
View File

@ -0,0 +1,76 @@
/**
* Fiber Direkt Shared Theme Utilities
*
* Extracts the repeated sectionBgMap / headingColor / bodyColor logic
* that was duplicated across 15+ block components into a single source of truth.
*
* Usage in any block:
* import { sectionBg, isExplicitDark, headingColor, bodyColor, mutedColor } from '@/utilities/fdTheme'
*
* const bg = sectionBg(sectionBackground)
* const dark = isExplicitDark(sectionBackground)
* const hClr = headingColor(dark)
*/
/* ── Section backgrounds ─────────────────────────────────────────────── */
const sectionBgMap: Record<string, string> = {
white: 'bg-white dark:bg-fd-navy',
gray: 'bg-fd-gray-light dark:bg-fd-navy',
navy: 'bg-fd-navy',
yellow: 'bg-fd-yellow',
navyGradient: 'bg-gradient-to-br from-fd-navy via-[#153350] to-fd-navy',
transparent: 'bg-transparent',
}
export const sectionBg = (bg?: string | null): string =>
sectionBgMap[bg || 'white'] || sectionBgMap.white
/* ── Dark detection ──────────────────────────────────────────────────── */
export const isExplicitDark = (bg?: string | null): boolean =>
bg === 'navy' || bg === 'navyGradient'
/* ── Text colors ─────────────────────────────────────────────────────── */
/** Heading: yellow on dark, navy on light (auto-adapts to OS dark mode) */
export const headingColor = (dark: boolean): string =>
dark ? 'text-fd-yellow' : 'text-fd-navy dark:text-fd-yellow'
/** Body: white on dark, navy on light */
export const bodyColor = (dark: boolean): string =>
dark ? 'text-white' : 'text-fd-navy dark:text-white'
/** Muted / secondary body text */
export const mutedColor = (dark: boolean): string =>
dark ? 'text-white/60' : 'text-fd-navy/60 dark:text-white/60'
/** Subdued body (80% opacity) */
export const bodySubduedColor = (dark: boolean): string =>
dark ? 'text-white/80' : 'text-fd-navy/80 dark:text-white/80'
/** Border color */
export const borderColor = (dark: boolean): string =>
dark ? 'border-white/20' : 'border-fd-navy/10 dark:border-white/20'
/** Link color */
export const linkColor = (dark: boolean): string =>
dark
? 'underline underline-offset-2 hover:text-fd-yellow transition-colors'
: 'underline underline-offset-2 hover:text-fd-navy dark:hover:text-fd-yellow transition-colors'
/** Label text (for form labels etc.) */
export const labelColor = (dark: boolean): string =>
dark ? 'text-white' : 'text-fd-navy dark:text-white'
/** Error text */
export const errorColor = (dark: boolean): string =>
dark ? 'text-red-300' : 'text-red-600 dark:text-red-300'
/* ── Responsive border-radius ────────────────────────────────────────── */
/** Signature FD card radius — responsive so 375px screens don't get capsule cards */
export const fdCardRadius = 'rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
/** Smaller radius for sub-elements (images inside cards, data tables, etc.) */
export const fdCardRadiusSm = 'rounded-[20px] md:rounded-[30px] lg:rounded-[40px]'

File diff suppressed because one or more lines are too long