Design system polish: borders, radius, typography, calculator, overlay, announcement bar + new services calculator
This commit is contained in:
parent
606a4c27b4
commit
f838dc1c6e
3
.gitignore
vendored
3
.gitignore
vendored
@ -17,3 +17,6 @@ src/**/*.bak.*
|
||||
next-env.d.ts
|
||||
tsconfig.tsbuildinfo
|
||||
media/
|
||||
src/migrations/*.ts
|
||||
src/migrations/*.json
|
||||
!src/migrations/index.ts
|
||||
|
||||
@ -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"
|
||||
BIN
public/fonts/fs-joey-bold.woff2
Normal file
BIN
public/fonts/fs-joey-bold.woff2
Normal file
Binary file not shown.
BIN
public/fonts/fs-joey-heavy.woff2
Normal file
BIN
public/fonts/fs-joey-heavy.woff2
Normal file
Binary file not shown.
BIN
public/fonts/fs-joey-italic.woff2
Normal file
BIN
public/fonts/fs-joey-italic.woff2
Normal file
Binary file not shown.
BIN
public/fonts/fs-joey-light.woff2
Normal file
BIN
public/fonts/fs-joey-light.woff2
Normal file
Binary file not shown.
BIN
public/fonts/fs-joey-medium.woff2
Normal file
BIN
public/fonts/fs-joey-medium.woff2
Normal file
Binary file not shown.
BIN
public/fonts/fs-joey-regular.woff2
Normal file
BIN
public/fonts/fs-joey-regular.woff2
Normal file
Binary file not shown.
@ -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}
|
||||
|
||||
@ -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'
|
||||
}`}
|
||||
|
||||
@ -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
|
||||
============================================
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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 så 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 || ''}
|
||||
/>
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,21 +43,30 @@ export const FDFaqBlockComponent: React.FC<FDFaqBlockProps> = ({
|
||||
{heading}
|
||||
</h2>
|
||||
<div className="w-full max-w-[1162px]">
|
||||
{items?.map((item, index) => (
|
||||
{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(openIndex === index ? null : index)}
|
||||
aria-expanded={openIndex === index}
|
||||
onClick={() => setOpenIndex(isOpen ? null : index)}
|
||||
aria-expanded={isOpen}
|
||||
aria-controls={panelId}
|
||||
>
|
||||
<svg
|
||||
className={`w-4 h-4 md:w-5 md:h-5 flex-shrink-0 transition-transform duration-200 ${
|
||||
openIndex === index ? 'rotate-45' : ''
|
||||
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" />
|
||||
@ -65,21 +76,24 @@ export const FDFaqBlockComponent: React.FC<FDFaqBlockProps> = ({
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
id={panelId}
|
||||
role="region"
|
||||
aria-labelledby={triggerId}
|
||||
className={`grid transition-all duration-200 ease-out ${
|
||||
openIndex === index
|
||||
isOpen
|
||||
? '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} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -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> = ({
|
||||
|
||||
@ -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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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 = (
|
||||
<>
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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}
|
||||
|
||||
371
src/blocks/FDServiceCalculatorBlock/Component.tsx
Normal file
371
src/blocks/FDServiceCalculatorBlock/Component.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
196
src/blocks/FDServiceCalculatorBlock/config.ts
Normal file
196
src/blocks/FDServiceCalculatorBlock/config.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -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={{
|
||||
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}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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 ${
|
||||
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}
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -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}`}>
|
||||
“{t.quote}”
|
||||
</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}`}>
|
||||
“{t.quote}”
|
||||
</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`}>
|
||||
“
|
||||
</span>
|
||||
|
||||
@ -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' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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";`)
|
||||
}
|
||||
@ -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 {}
|
||||
|
||||
@ -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å på/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 på 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
76
src/utilities/fdTheme.ts
Normal 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
Loading…
Reference in New Issue
Block a user