feat: OS-based dark mode across all blocks

This commit is contained in:
Jeffrey 2026-02-20 07:57:47 +01:00
parent b36f7ebe6e
commit 0bf56a1388
13 changed files with 177 additions and 252 deletions

View File

@ -13,7 +13,6 @@ interface HeaderClientProps {
data: Header data: Header
} }
/** Resolves the logo href from the logoLink group field */
function resolveLogoHref(logoLink: Header['logoLink']): string { function resolveLogoHref(logoLink: Header['logoLink']): string {
if (!logoLink) return '/' if (!logoLink) return '/'
if (logoLink.type === 'reference') { if (logoLink.type === 'reference') {
@ -28,11 +27,19 @@ function resolveLogoHref(logoLink: Header['logoLink']): string {
} }
export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => { export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
/* Storing the value in a useState to avoid hydration errors */
const [theme, setTheme] = useState<string | null>(null) const [theme, setTheme] = useState<string | null>(null)
const [isDark, setIsDark] = useState(false)
const { headerTheme, setHeaderTheme } = useHeaderTheme() const { headerTheme, setHeaderTheme } = useHeaderTheme()
const pathname = usePathname() const pathname = usePathname()
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)')
setIsDark(mq.matches)
const handler = (e: MediaQueryListEvent) => setIsDark(e.matches)
mq.addEventListener('change', handler)
return () => mq.removeEventListener('change', handler)
}, [])
useEffect(() => { useEffect(() => {
setHeaderTheme(null) setHeaderTheme(null)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -47,12 +54,12 @@ export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
return ( return (
<header <header
className="relative z-20 w-full bg-white" className="relative z-20 w-full bg-white dark:bg-fd-navy border-b border-transparent dark:border-white/10"
{...(theme ? { 'data-theme': theme } : {})} {...(theme ? { 'data-theme': theme } : {})}
> >
<div className="container py-5 flex justify-between items-center"> <div className="container py-5 flex justify-between items-center">
<Link href={logoHref}> <Link href={logoHref}>
<Logo loading="eager" priority="high" variant="blue" className="max-w-[80px] md:max-w-[100px]" /> <Logo loading="eager" priority="high" variant={isDark ? "white" : "blue"} className="max-w-[80px] md:max-w-[100px]" />
</Link> </Link>
<HeaderNav data={data} /> <HeaderNav data={data} />
</div> </div>

View File

@ -5,11 +5,9 @@ import Link from 'next/link'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import { MenuIcon, XIcon, ChevronDownIcon, ChevronRightIcon } from 'lucide-react' import { MenuIcon, XIcon, ChevronDownIcon, ChevronRightIcon } from 'lucide-react'
// ─── Types ────────────────────────────────────────────────────────────────────
type NavChild = NonNullable<NonNullable<NonNullable<HeaderType['navItems']>[number]['children']>[number]> type NavChild = NonNullable<NonNullable<NonNullable<HeaderType['navItems']>[number]['children']>[number]>
type NavItem = NonNullable<HeaderType['navItems']>[number] type NavItem = NonNullable<HeaderType['navItems']>[number]
// ─── Resolve href from reference or custom url ────────────────────────────────
function resolveHref(item: { function resolveHref(item: {
type?: string | null type?: string | null
url?: string | null url?: string | null
@ -25,7 +23,6 @@ function resolveHref(item: {
return item.url || '#' return item.url || '#'
} }
// ─── Swedish Flag ─────────────────────────────────────────────────────────────
const SwedishFlag = () => ( const SwedishFlag = () => (
<svg width="28" height="20" viewBox="0 0 28 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" className="rounded-[3px]"> <svg width="28" height="20" viewBox="0 0 28 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" className="rounded-[3px]">
<rect width="28" height="20" fill="#006AA7" /> <rect width="28" height="20" fill="#006AA7" />
@ -34,7 +31,6 @@ const SwedishFlag = () => (
</svg> </svg>
) )
// ─── Group children into columns ──────────────────────────────────────────────
function groupChildren(children: NavChild[]): { heading: string | null; links: NavChild[] }[] { function groupChildren(children: NavChild[]): { heading: string | null; links: NavChild[] }[] {
const main: NavChild[] = [] const main: NavChild[] = []
const groups: Record<string, NavChild[]> = {} const groups: Record<string, NavChild[]> = {}
@ -58,7 +54,6 @@ function groupChildren(children: NavChild[]): { heading: string | null; links: N
return result return result
} }
// ─── Mega Menu Panel ──────────────────────────────────────────────────────────
const MegaMenuPanel: React.FC<{ const MegaMenuPanel: React.FC<{
item: NavItem item: NavItem
headerRef: React.RefObject<HTMLElement | null> headerRef: React.RefObject<HTMLElement | null>
@ -94,18 +89,18 @@ const MegaMenuPanel: React.FC<{
onClick={onClose} onClick={onClose}
/> />
{/* Panel */} {/* Panel — white in light, navy in dark */}
<div <div
ref={panelRef} ref={panelRef}
className="fixed left-0 right-0 z-40" className="fixed left-0 right-0 z-40 bg-white dark:bg-fd-navy border-b border-fd-navy/10 dark:border-white/10"
style={{ top, backgroundColor: '#ffffff' }} style={{ top }}
> >
<div className="max-w-[1200px] mx-auto px-6 md:px-8 py-10 md:py-14"> <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-[200px_1fr] gap-12 md:gap-20">
{/* Left: category title */} {/* Left: category title */}
<div> <div>
<h2 className="font-joey-heavy text-fd-h1 text-fd-navy leading-tight"> <h2 className="font-joey-heavy text-fd-h1 text-fd-navy dark:text-fd-yellow leading-tight">
{item.label} {item.label}
</h2> </h2>
</div> </div>
@ -115,7 +110,7 @@ const MegaMenuPanel: React.FC<{
{columns.map((col, ci) => ( {columns.map((col, ci) => (
<div key={ci}> <div key={ci}>
{col.heading && ( {col.heading && (
<p className="font-joey-medium text-fd-small text-fd-navy/40 uppercase tracking-widest mb-4"> <p className="font-joey-medium text-fd-small text-fd-navy/40 dark:text-white/40 uppercase tracking-widest mb-4">
{col.heading} {col.heading}
</p> </p>
)} )}
@ -125,10 +120,10 @@ const MegaMenuPanel: React.FC<{
<Link <Link
href={resolveHref(link)} href={resolveHref(link)}
onClick={onClose} onClick={onClose}
className={`block py-1.5 transition-colors duration-150 hover:text-fd-yellow ${ className={`block py-1.5 transition-colors duration-150 ${
col.heading col.heading
? 'font-joey text-fd-body text-fd-navy/75 hover:text-fd-navy' ? 'font-joey text-fd-body text-fd-navy/75 dark:text-white/70 hover:text-fd-navy dark:hover:text-fd-yellow'
: 'font-joey-bold text-fd-h3 text-fd-navy' : 'font-joey-bold text-fd-h3 text-fd-navy dark:text-white hover:text-fd-yellow dark:hover:text-fd-yellow'
}`} }`}
> >
{link.label} {link.label}
@ -147,7 +142,6 @@ const MegaMenuPanel: React.FC<{
) )
} }
// ─── Main Nav ─────────────────────────────────────────────────────────────────
export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => { export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
const navItems = (data?.navItems || []) as NavItem[] const navItems = (data?.navItems || []) as NavItem[]
const [mobileOpen, setMobileOpen] = useState(false) const [mobileOpen, setMobileOpen] = useState(false)
@ -208,7 +202,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
return ( return (
<> <>
{/* ── Desktop nav ──────────────────────────────────────────────────── */} {/* ── Desktop nav ── */}
<nav ref={navRef} className="hidden md:flex gap-6 items-center"> <nav ref={navRef} className="hidden md:flex gap-6 items-center">
{navItems.map((item, i) => { {navItems.map((item, i) => {
const hasChildren = item.children && item.children.length > 0 const hasChildren = item.children && item.children.length > 0
@ -220,7 +214,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
<button <button
onClick={() => setOpenDropdown(isOpen ? null : item.label ?? null)} onClick={() => setOpenDropdown(isOpen ? null : item.label ?? null)}
className={`flex items-center gap-1 font-joey-bold transition-colors text-base ${ className={`flex items-center gap-1 font-joey-bold transition-colors text-base ${
isOpen ? 'text-fd-yellow' : 'text-fd-navy hover:text-fd-yellow' isOpen ? 'text-fd-yellow' : 'text-fd-navy dark:text-white hover:text-fd-yellow dark:hover:text-fd-yellow'
}`} }`}
aria-haspopup="true" aria-haspopup="true"
aria-expanded={isOpen} aria-expanded={isOpen}
@ -231,10 +225,10 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
/> />
</button> </button>
{/* Small card dropdown (non-mega only) */} {/* Small card dropdown */}
{!isMega && ( {!isMega && (
<div <div
className={`absolute top-full left-0 mt-3 min-w-[220px] bg-white rounded-[16px] shadow-lg border border-fd-navy/10 py-2 z-50 transition-all duration-200 origin-top ${ className={`absolute top-full left-0 mt-3 min-w-[220px] bg-white dark:bg-fd-navy rounded-[16px] shadow-lg border border-fd-navy/10 dark:border-white/10 py-2 z-50 transition-all duration-200 origin-top ${
isOpen isOpen
? 'opacity-100 scale-y-100 pointer-events-auto' ? 'opacity-100 scale-y-100 pointer-events-auto'
: 'opacity-0 scale-y-95 pointer-events-none' : 'opacity-0 scale-y-95 pointer-events-none'
@ -245,7 +239,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
key={j} key={j}
href={resolveHref(child)} href={resolveHref(child)}
onClick={() => setOpenDropdown(null)} onClick={() => setOpenDropdown(null)}
className="block px-5 py-2.5 font-joey text-fd-navy hover:bg-fd-yellow/20 transition-colors text-sm" className="block px-5 py-2.5 font-joey text-fd-navy dark:text-white hover:bg-fd-yellow/20 dark:hover:bg-white/10 transition-colors text-sm"
> >
{child.label} {child.label}
</Link> </Link>
@ -257,7 +251,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
<Link <Link
key={i} key={i}
href={resolveHref(item)} href={resolveHref(item)}
className="font-joey-bold text-fd-navy hover:text-fd-yellow transition-colors text-base" className="font-joey-bold text-fd-navy dark:text-white hover:text-fd-yellow dark:hover:text-fd-yellow transition-colors text-base"
> >
{item.label} {item.label}
</Link> </Link>
@ -267,7 +261,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
<SwedishFlag /> <SwedishFlag />
</nav> </nav>
{/* ── Mega menu panel ───────────────────────────────────────────────── */} {/* ── Mega menu panel ── */}
{activeItem?.megaMenu && activeItem.children?.length && ( {activeItem?.megaMenu && activeItem.children?.length && (
<MegaMenuPanel <MegaMenuPanel
item={activeItem} item={activeItem}
@ -277,16 +271,16 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
/> />
)} )}
{/* ── Mobile hamburger ─────────────────────────────────────────────── */} {/* ── Mobile hamburger ── */}
<button <button
className="md:hidden text-fd-navy p-2" className="md:hidden text-fd-navy dark:text-white p-2"
onClick={() => setMobileOpen(!mobileOpen)} onClick={() => setMobileOpen(!mobileOpen)}
aria-label={mobileOpen ? 'Stäng meny' : 'Öppna meny'} aria-label={mobileOpen ? 'Stäng meny' : 'Öppna meny'}
> >
{mobileOpen ? <XIcon className="w-6 h-6" /> : <MenuIcon className="w-6 h-6" />} {mobileOpen ? <XIcon className="w-6 h-6" /> : <MenuIcon className="w-6 h-6" />}
</button> </button>
{/* ── Mobile full-screen overlay ───────────────────────────────────── */} {/* ── Mobile full-screen overlay — always navy, no dark: needed ── */}
<div <div
className={`fixed inset-0 z-50 bg-fd-navy flex flex-col transition-transform duration-300 md:hidden ${ 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' mobileOpen ? 'translate-x-0' : 'translate-x-full'

View File

@ -3,7 +3,7 @@
@config '../../../tailwind.config.mjs'; @config '../../../tailwind.config.mjs';
@custom-variant dark (&:is([data-theme='dark'] *)); @custom-variant dark (@media (prefers-color-scheme: dark));
@custom-variant sm (@media (width >= theme(--breakpoint-sm))); @custom-variant sm (@media (width >= theme(--breakpoint-sm)));
@custom-variant md (@media (width >= theme(--breakpoint-md))); @custom-variant md (@media (width >= theme(--breakpoint-md)));
@custom-variant lg (@media (width >= theme(--breakpoint-lg))); @custom-variant lg (@media (width >= theme(--breakpoint-lg)));
@ -225,7 +225,8 @@ html {
} }
html[data-theme='dark'], html[data-theme='dark'],
html[data-theme='light'] { html[data-theme='light'],
html:not([data-theme]) {
opacity: initial; opacity: initial;
} }
@ -719,6 +720,27 @@ html[data-theme='light'] {
--fd-border-strong: #245580; --fd-border-strong: #245580;
} }
} }
/* Button dark mode overrides
fd-btn-primary hover: navywhite (visible on dark bg)
fd-btn-secondary: flip to white border/text (invisible on navy otherwise)
*/
@media (prefers-color-scheme: dark) {
.fd-btn-primary:hover {
background-color: #ffffff;
color: var(--color-fd-navy);
}
.fd-btn-secondary {
color: #ffffff;
border-color: #ffffff;
}
.fd-btn-secondary:hover {
background-color: #ffffff;
color: var(--color-fd-navy);
}
}
/* FD Prose styles (for Posts rich text) /* FD Prose styles (for Posts rich text)
Add these to your existing globals.css Add these to your existing globals.css
*/ */
@ -728,6 +750,30 @@ html[data-theme='light'] {
color: theme('colors.fd-navy'); color: theme('colors.fd-navy');
} }
@media (prefers-color-scheme: dark) {
.fd-prose {
color: #ffffff;
}
.fd-prose h1,
.fd-prose h2,
.fd-prose h3,
.fd-prose h4 {
color: theme('colors.fd-yellow');
}
.fd-prose a {
color: theme('colors.fd-yellow');
}
.fd-prose a:hover {
color: #ffffff;
}
.fd-prose blockquote {
color: rgba(255,255,255,0.8);
}
.fd-prose hr {
border-color: rgba(255,255,255,0.15);
}
}
.fd-prose h1, .fd-prose h1,
.fd-prose h2, .fd-prose h2,
.fd-prose h3, .fd-prose h3,

View File

@ -43,13 +43,14 @@ export default async function RootLayout({ children }: { children: React.ReactNo
className={cn(GeistSans.variable, GeistMono.variable)} className={cn(GeistSans.variable, GeistMono.variable)}
lang="sv" lang="sv"
suppressHydrationWarning suppressHydrationWarning
style={{ colorScheme: 'light' }} style={{ colorScheme: 'light dark' }}
> >
<head> <head>
<InitTheme /> <InitTheme />
<link href="/favicon.ico" rel="icon" sizes="32x32" /> <link href="/favicon.ico" rel="icon" sizes="32x32" />
<link href="/favicon.svg" rel="icon" type="image/svg+xml" /> <link href="/favicon.svg" rel="icon" type="image/svg+xml" />
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#0e2338" media="(prefers-color-scheme: dark)" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" /> <meta name="apple-mobile-web-app-status-bar-style" content="default" />
<HeadInjection siteSettings={siteSettings} /> <HeadInjection siteSettings={siteSettings} />
</head> </head>

View File

@ -31,33 +31,28 @@ export default async function PostPage({ params }: Args) {
const summary = post.meta?.description ?? null const summary = post.meta?.description ?? null
return ( return (
<article className="min-h-screen bg-white"> <article className="min-h-screen bg-white dark:bg-fd-navy">
{/* ── Main column ── */}
<div className="max-w-[1200px] mx-auto px-6 md:px-8 py-12 md:py-16 lg:py-20"> <div className="max-w-[1200px] mx-auto px-6 md:px-8 py-12 md:py-16 lg:py-20">
<div className="max-w-[640px]"> <div className="max-w-[640px]">
{/* Back link */}
<a <a
href="/posts" href="/posts"
className="inline-flex items-center gap-2 font-joey text-sm text-fd-navy/50 hover:text-fd-navy transition-colors mb-8" className="inline-flex items-center gap-2 font-joey text-sm text-fd-navy/50 dark:text-white/50 hover:text-fd-navy dark:hover:text-white transition-colors mb-8"
> >
Tillbaka till nyheter Tillbaka till nyheter
</a> </a>
{/* Title */} <h1 className="font-joey-heavy text-[2rem] md:text-[2.5rem] lg:text-[2.875rem] leading-[1.1] text-fd-navy dark:text-fd-yellow mb-4">
<h1 className="font-joey-heavy text-[2rem] md:text-[2.5rem] lg:text-[2.875rem] leading-[1.1] text-fd-navy mb-4">
{post.title} {post.title}
</h1> </h1>
{/* Summary / excerpt (from meta description) */}
{summary && ( {summary && (
<p className="font-joey text-base md:text-lg text-fd-navy/70 leading-relaxed mb-8"> <p className="font-joey text-base md:text-lg text-fd-navy/70 dark:text-white/70 leading-relaxed mb-8">
{summary} {summary}
</p> </p>
)} )}
{/* Hero image */}
{heroImage?.url && ( {heroImage?.url && (
<figure className="mb-6"> <figure className="mb-6">
<div className="rounded-[20px] overflow-hidden"> <div className="rounded-[20px] overflow-hidden">
@ -69,26 +64,24 @@ export default async function PostPage({ params }: Args) {
/> />
</div> </div>
{heroImage.alt && heroImage.alt.trim() !== '' && ( {heroImage.alt && heroImage.alt.trim() !== '' && (
<figcaption className="mt-3 font-joey text-sm text-fd-navy/50 leading-snug"> <figcaption className="mt-3 font-joey text-sm text-fd-navy/50 dark:text-white/50 leading-snug">
{heroImage.alt} {heroImage.alt}
</figcaption> </figcaption>
)} )}
</figure> </figure>
)} )}
{/* Author + date */}
<div className="mb-8 font-joey text-sm"> <div className="mb-8 font-joey text-sm">
{authors.length > 0 && ( {authors.length > 0 && (
<p className="font-joey-bold text-fd-navy"> <p className="font-joey-bold text-fd-navy dark:text-white">
Av {authors.map((a: any) => a.name).join(', ')} Av {authors.map((a: any) => a.name).join(', ')}
</p> </p>
)} )}
{post.publishedAt && ( {post.publishedAt && (
<p className="text-fd-navy/50">{formatDateTime(post.publishedAt)}</p> <p className="text-fd-navy/50 dark:text-white/50">{formatDateTime(post.publishedAt)}</p>
)} )}
</div> </div>
{/* Rich text body */}
{post.content && ( {post.content && (
<div className="mb-12"> <div className="mb-12">
<RichText <RichText
@ -99,7 +92,6 @@ export default async function PostPage({ params }: Args) {
</div> </div>
)} )}
{/* CTA */}
<a href="/kontakt" className="fd-btn-primary"> <a href="/kontakt" className="fd-btn-primary">
Kontakta oss Kontakta oss
</a> </a>
@ -110,7 +102,6 @@ export default async function PostPage({ params }: Args) {
) )
} }
// ─── Metadata ─────────────────────────────────────────────────────────────────
export async function generateMetadata({ params }: Args): Promise<Metadata> { export async function generateMetadata({ params }: Args): Promise<Metadata> {
const { slug } = await params const { slug } = await params
const payload = await getPayload({ config }) const payload = await getPayload({ config })
@ -128,7 +119,6 @@ export async function generateMetadata({ params }: Args): Promise<Metadata> {
return generateMeta({ doc: post }) return generateMeta({ doc: post })
} }
// ─── Static params ────────────────────────────────────────────────────────────
export async function generateStaticParams() { export async function generateStaticParams() {
const payload = await getPayload({ config }) const payload = await getPayload({ config })
const posts = await payload.find({ const posts = await payload.find({

View File

@ -28,19 +28,17 @@ export default async function Page() {
}) })
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-white dark:bg-fd-navy">
<PageClient /> <PageClient />
<div className="max-w-[1200px] mx-auto px-6 md:px-8 pt-12 md:pt-16 pb-16 md:pb-24"> <div className="max-w-[1200px] mx-auto px-6 md:px-8 pt-12 md:pt-16 pb-16 md:pb-24">
{/* Page heading */} <h1 className="font-joey-heavy text-4xl md:text-5xl lg:text-[3.25rem] text-fd-navy dark:text-fd-yellow mb-10 md:mb-14">
<h1 className="font-joey-heavy text-4xl md:text-5xl lg:text-[3.25rem] text-fd-navy mb-10 md:mb-14">
Nyheter! Nyheter!
</h1> </h1>
{/* Grid */}
{posts.docs.length === 0 ? ( {posts.docs.length === 0 ? (
<p className="font-joey text-fd-navy/50 py-16">Inga inlägg hittades.</p> <p className="font-joey text-fd-navy/50 dark:text-white/50 py-16">Inga inlägg hittades.</p>
) : ( ) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-12"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-12">
{posts.docs.map((post) => { {posts.docs.map((post) => {
@ -53,20 +51,17 @@ export default async function Page() {
href={`/posts/${p.slug}`} href={`/posts/${p.slug}`}
className="group flex flex-col" className="group flex flex-col"
> >
{/* Title above image */} <h2 className="font-joey-bold text-fd-navy dark:text-white text-xl md:text-[1.375rem] leading-snug mb-2 group-hover:text-fd-navy/70 dark:group-hover:text-fd-yellow transition-colors line-clamp-2">
<h2 className="font-joey-bold text-fd-navy text-xl md:text-[1.375rem] leading-snug mb-2 group-hover:text-fd-navy/70 transition-colors line-clamp-2">
{p.title} {p.title}
</h2> </h2>
{/* Date between title and image */}
{p.publishedAt && ( {p.publishedAt && (
<p className="font-joey text-sm text-fd-navy/50 mb-4"> <p className="font-joey text-sm text-fd-navy/50 dark:text-white/50 mb-4">
{formatDateTime(p.publishedAt)} {formatDateTime(p.publishedAt)}
</p> </p>
)} )}
{/* Image with rounded corners */} <div className="relative aspect-[4/3] rounded-[20px] overflow-hidden bg-fd-navy/5 dark:bg-white/5">
<div className="relative aspect-[4/3] rounded-[20px] overflow-hidden bg-fd-navy/5">
{heroImage?.url ? ( {heroImage?.url ? (
<FDImage <FDImage
media={heroImage} media={heroImage}
@ -78,7 +73,7 @@ export default async function Page() {
/> />
) : ( ) : (
<div className="w-full h-full flex items-center justify-center"> <div className="w-full h-full flex items-center justify-center">
<span className="font-joey text-fd-navy/30 text-sm">Ingen bild</span> <span className="font-joey text-fd-navy/30 dark:text-white/30 text-sm">Ingen bild</span>
</div> </div>
)} )}
</div> </div>
@ -88,7 +83,6 @@ export default async function Page() {
</div> </div>
)} )}
{/* Pagination */}
{posts.totalPages > 1 && posts.page && ( {posts.totalPages > 1 && posts.page && (
<div className="mt-16"> <div className="mt-16">
<Pagination page={posts.page} totalPages={posts.totalPages} /> <Pagination page={posts.page} totalPages={posts.totalPages} />

Binary file not shown.

View File

@ -13,10 +13,10 @@ const cardStyleMap: Record<
border: '', border: '',
}, },
gray: { gray: {
bg: 'bg-[#e5e5e5]', bg: 'bg-[#e5e5e5] dark:bg-white/10',
headingText: 'text-fd-navy', headingText: 'text-fd-navy dark:text-fd-yellow',
bodyText: 'text-fd-navy', bodyText: 'text-fd-navy dark:text-white',
linkText: 'text-fd-navy hover:text-fd-navy/70', linkText: 'text-fd-navy hover:text-fd-navy/70 dark:text-fd-yellow dark:hover:text-fd-yellow/80',
border: '', border: '',
}, },
yellow: { yellow: {
@ -34,18 +34,18 @@ const cardStyleMap: Record<
border: '', border: '',
}, },
outlined: { outlined: {
bg: 'bg-white', bg: 'bg-white dark:bg-white/10',
headingText: 'text-fd-navy', headingText: 'text-fd-navy dark:text-fd-yellow',
bodyText: 'text-fd-navy', bodyText: 'text-fd-navy dark:text-white',
linkText: 'text-fd-navy/70 hover:text-fd-navy', 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 shadow-sm', border: 'border-6 border-gray-200 dark:border-white/10 shadow-sm',
}, },
} }
const sectionBgMap: Record<string, string> = { const sectionBgMap: Record<string, string> = {
white: 'bg-white', white: 'bg-white dark:bg-fd-navy',
navy: 'bg-fd-navy', navy: 'bg-fd-navy',
gray: 'bg-fd-gray-light', gray: 'bg-fd-gray-light dark:bg-fd-navy',
} }
const layoutGridMap: Record<string, string> = { const layoutGridMap: Record<string, string> = {

View File

@ -29,8 +29,8 @@ export const FDHeroBlockComponent: React.FC<FDHeroBlockProps> = ({
headingColor = 'text-fd-navy' headingColor = 'text-fd-navy'
textBodyColor = 'text-fd-navy' textBodyColor = 'text-fd-navy'
} else { } else {
headingColor = isDark ? 'text-fd-yellow' : 'text-fd-navy' headingColor = isDark ? 'text-fd-yellow' : 'text-fd-navy dark:text-fd-yellow'
textBodyColor = isDark ? 'text-white' : 'text-fd-navy' textBodyColor = isDark ? 'text-white' : 'text-fd-navy dark:text-white'
} }
const overlayClass = const overlayClass =
@ -40,7 +40,9 @@ export const FDHeroBlockComponent: React.FC<FDHeroBlockProps> = ({
return ( return (
<section <section
className={`relative w-full py-16 md:py-20 lg:py-[99px] ${hasBgImage ? '' : isDark ? 'bg-fd-navy' : 'bg-white'} overflow-hidden`} className={`relative w-full py-16 md:py-20 lg:py-[99px] ${
hasBgImage ? '' : isDark ? 'bg-fd-navy' : 'bg-white dark:bg-fd-navy'
} overflow-hidden`}
> >
{hasBgImage && ( {hasBgImage && (
<> <>

View File

@ -13,10 +13,10 @@ export const FDServiceChooserBlockComponent: React.FC<Props> = ({
const prevIndex = useRef(0) const prevIndex = useRef(0)
const isDark = sectionBackground === 'navy' const isDark = sectionBackground === 'navy'
const bgClass = isDark ? 'bg-fd-navy' : sectionBackground === 'gray' ? 'bg-fd-surface-alt' : 'bg-white' 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' const titleClass = isDark ? 'text-fd-yellow' : 'text-fd-navy dark:text-fd-yellow'
const bodyClass = isDark ? 'text-white' : 'text-fd-navy' 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' 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 handleTabChange = (i: number) => { const handleTabChange = (i: number) => {
if (i === activeIndex) return if (i === activeIndex) return
@ -57,7 +57,7 @@ export const FDServiceChooserBlockComponent: React.FC<Props> = ({
? 'bg-fd-yellow border-fd-yellow text-fd-navy shadow-sm' ? 'bg-fd-yellow border-fd-yellow text-fd-navy shadow-sm'
: isDark : isDark
? 'border-white/30 text-white hover:border-fd-yellow hover:text-fd-yellow' ? 'border-white/30 text-white hover:border-fd-yellow hover:text-fd-yellow'
: 'border-fd-navy/20 text-fd-navy hover:border-fd-yellow hover:bg-fd-yellow/10' : 'border-fd-navy/20 text-fd-navy hover:border-fd-yellow hover:bg-fd-yellow/10 dark:border-white/30 dark:text-white dark:hover:border-fd-yellow dark:hover:text-fd-yellow'
}`} }`}
> >
{cat.label} {cat.label}
@ -87,7 +87,7 @@ export const FDServiceChooserBlockComponent: React.FC<Props> = ({
transform: animating ? 'translateY(8px)' : 'translateY(0)', transform: animating ? 'translateY(8px)' : 'translateY(0)',
}} }}
> >
<h3 className={`font-joey-bold text-fd-h3 ${isDark ? 'text-fd-yellow' : 'text-fd-navy'}`}> <h3 className={`font-joey-bold text-fd-h3 ${isDark ? 'text-fd-yellow' : 'text-fd-navy dark:text-fd-yellow'}`}>
{service.title} {service.title}
</h3> </h3>
{service.description && ( {service.description && (
@ -99,7 +99,7 @@ export const FDServiceChooserBlockComponent: React.FC<Props> = ({
<div className="mt-auto pt-2"> <div className="mt-auto pt-2">
<a <a
href={service.ctaLink || '/kontakt'} href={service.ctaLink || '/kontakt'}
className={isDark ? 'fd-btn-secondary-dark' : 'fd-btn-secondary'} className={isDark ? 'fd-btn-secondary-dark' : 'fd-btn-secondary dark:fd-btn-secondary-dark'}
> >
{service.ctaText} {service.ctaText}
</a> </a>

View File

@ -12,104 +12,59 @@ const DEFAULT_PRICING = {
adminFee: 200, adminFee: 200,
} }
const formatKr = (n: number) => { const formatKr = (n: number) => Math.round(n).toLocaleString('sv-SE') + ' kr'
// Format like "2 495 kr" with space as thousands separator
return Math.round(n).toLocaleString('sv-SE') + ' kr'
}
// ─── Toggle switch ───────────────────────────────────────────────────────────
function Toggle({ active, onToggle }: { active: boolean; onToggle: () => void }) { function Toggle({ active, onToggle }: { active: boolean; onToggle: () => void }) {
return ( return (
<button <button
type="button" type="button"
onClick={onToggle} onClick={onToggle}
style={{ display: 'block', width: '3.5rem', height: '2rem', flexShrink: 0 }} style={{ display: 'block', width: '3.5rem', height: '2rem', flexShrink: 0 }}
className={`relative rounded-full transition-all duration-300 ${ className={`relative rounded-full transition-all duration-300 ${active ? 'bg-fd-yellow' : 'bg-[#e2e8f0] dark:bg-white/20'}`}
active ? 'bg-fd-yellow' : 'bg-[#e2e8f0]'
}`}
role="switch" role="switch"
aria-checked={active} aria-checked={active}
> >
<div <div className={`absolute top-1 w-6 h-6 rounded-full bg-white shadow-sm transition-all duration-300 ${active ? 'left-7' : 'left-1'}`} />
className={`absolute top-1 w-6 h-6 rounded-full bg-white shadow-sm transition-all duration-300 ${
active ? 'left-7' : 'left-1'
}`}
/>
</button> </button>
) )
} }
// ─── Number stepper row ──────────────────────────────────────────────────────
function ResourceRow({ function ResourceRow({
label, label, value, onChange, min = 0, max = 999, step = 1, unit, priceLabel, isDark,
value,
onChange,
min = 0,
max = 999,
step = 1,
unit,
priceLabel,
isDark,
}: { }: {
label: string label: string; value: number; onChange: (v: number) => void
value: number min?: number; max?: number; step?: number; unit: string; priceLabel: string; isDark: boolean
onChange: (v: number) => void
min?: number
max?: number
step?: number
unit: string
priceLabel: string
isDark: boolean
}) { }) {
const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(14,35,56,0.08)'
const labelColor = isDark ? 'text-white' : 'text-fd-navy'
const subColor = isDark ? 'text-white/50' : 'text-fd-navy/50'
const btnColor = isDark
? 'border-white/30 text-white hover:border-white/70'
: 'border-fd-navy/20 text-fd-navy hover:border-fd-navy/60'
const inputColor = isDark
? 'bg-white/10 border-white/20 text-white'
: 'bg-fd-surface-alt border-fd-navy/15 text-fd-navy'
const unitColor = isDark ? 'text-white/40' : 'text-fd-navy/40'
return ( return (
<div <div className="flex items-center justify-between py-4 border-b border-fd-navy/10 dark:border-white/10">
className="flex items-center justify-between py-4"
style={{ borderBottom: `1px solid ${borderColor}` }}
>
<div className="flex flex-col"> <div className="flex flex-col">
<span className={`font-joey-bold text-fd-body ${labelColor}`}>{label}</span> <span className="font-joey-bold text-fd-body text-fd-navy dark:text-white">{label}</span>
<span className={`font-joey text-fd-small ${subColor}`}>{priceLabel}</span> <span className="font-joey text-fd-small text-fd-navy/50 dark:text-white/50">{priceLabel}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => onChange(Math.max(min, value - step))} 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 ${btnColor}`} 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"
> ></button>
</button>
<input <input
type="number" type="number"
value={value} value={value}
onChange={(e) => onChange(Math.max(min, Math.min(max, Number(e.target.value) || 0)))} 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 ${inputColor} [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none`} 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"
style={{ appearance: 'textfield' }} style={{ appearance: 'textfield' }}
/> />
<span className={`font-joey text-fd-small w-7 ${unitColor}`}>{unit}</span> <span className="font-joey text-fd-small w-7 text-fd-navy/40 dark:text-white/40">{unit}</span>
<button <button
type="button" type="button"
onClick={() => onChange(Math.min(max, value + step))} 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 ${btnColor}`} 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"
> >+</button>
+
</button>
</div> </div>
</div> </div>
) )
} }
// ─── Main component ──────────────────────────────────────────────────────────
export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps> = ({ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps> = ({
heading = 'Virtuell server — kalkylator', heading = 'Virtuell server — kalkylator',
description, description,
@ -153,46 +108,35 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
const ramCost = disc(ramGb * pricing.ramPerGb) const ramCost = disc(ramGb * pricing.ramPerGb)
const ssdCost = disc(ssdGb * pricing.ssdPerGb) const ssdCost = disc(ssdGb * pricing.ssdPerGb)
const hddCost = disc(hddGb * pricing.hddPerGb) const hddCost = disc(hddGb * pricing.hddPerGb)
// Admin fee is fixed — always on if enabled in CMS, not customer-controlled
const feeCost = showAdminFee ? feeAmount : 0 const feeCost = showAdminFee ? feeAmount : 0
const extraCosts = (additionalServices ?? []).map((svc, i) => const extraCosts = (additionalServices ?? []).map((svc, i) => extraToggles[i] ? (svc.price ?? 0) : 0)
extraToggles[i] ? (svc.price ?? 0) : 0
)
const extraTotal = extraCosts.reduce((a, b) => a + b, 0) const extraTotal = extraCosts.reduce((a, b) => a + b, 0)
const total = licenseCost + cpuCost + ramCost + ssdCost + hddCost + feeCost + extraTotal const total = licenseCost + cpuCost + ramCost + ssdCost + hddCost + feeCost + extraTotal
return { licenseCost, cpuCost, ramCost, ssdCost, hddCost, feeCost, extraCosts, total } return { licenseCost, cpuCost, ramCost, ssdCost, hddCost, feeCost, extraCosts, total }
}, [os, cpuCores, ramGb, ssdGb, hddGb, extraToggles, pricing, discount, feeAmount, showAdminFee, additionalServices]) }, [os, cpuCores, ramGb, ssdGb, hddGb, extraToggles, pricing, discount, feeAmount, showAdminFee, additionalServices])
const isDark = sectionBackground === 'navy' const isDark = sectionBackground === 'navy'
const bgClass = isDark ? 'bg-fd-navy' : sectionBackground === 'gray' ? 'bg-fd-surface-alt' : 'bg-white' const bgClass = isDark ? 'bg-fd-navy' : sectionBackground === 'gray' ? 'bg-fd-surface-alt dark:bg-fd-navy' : 'bg-white dark:bg-fd-navy'
// Card styling // Use Tailwind classes for card styling instead of inline styles
const cardBg = isDark ? 'rgba(255,255,255,0.05)' : '#ffffff' const cardClass = isDark
const cardBorder = isDark ? 'rgba(255,255,255,0.12)' : '#e2e8f0' ? 'bg-white/5 border-[6px] border-white/10 rounded-[clamp(28px,4vw,60px)]'
const cardStyle: React.CSSProperties = { : 'bg-white border-[6px] border-[#e2e8f0] rounded-[clamp(28px,4vw,60px)] dark:bg-white/5 dark:border-white/10'
background: cardBg,
border: `6px solid ${cardBorder}`,
borderRadius: 'clamp(28px, 4vw, 60px)',
}
// Colors const headingColor = isDark ? 'text-white' : 'text-fd-navy dark:text-white'
const headingColor = isDark ? 'text-white' : 'text-fd-navy' const descColor = isDark ? 'text-white/60' : 'text-fd-navy/60 dark:text-white/60'
const descColor = isDark ? 'text-white/60' : 'text-fd-navy/60' const sectionLabel = isDark ? 'text-white/40' : 'text-fd-navy/35 dark:text-white/40'
const sectionLabelColor = isDark ? 'text-white/40' : 'text-fd-navy/35' const summaryLabel = isDark ? 'text-white' : 'text-fd-navy dark:text-white'
const dividerColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(14,35,56,0.08)' const categoryLabel = isDark ? 'text-white/40' : 'text-fd-navy/40 dark:text-white/40'
const summaryLabelColor = isDark ? 'text-white' : 'text-fd-navy' const dividerClass = isDark ? 'border-white/10' : 'border-fd-navy/8 dark:border-white/10'
const summaryValueColor = isDark ? 'text-white' : 'text-fd-navy'
const categoryLabelColor = isDark ? 'text-white/40' : 'text-fd-navy/40'
// OS toggle const osActiveClass = 'bg-fd-yellow text-fd-navy border-fd-yellow font-joey-bold'
const osActiveClass = 'bg-fd-yellow text-fd-navy border-fd-yellow font-joey-bold'
const osInactiveClass = isDark const osInactiveClass = isDark
? 'bg-transparent text-white/70 border-white/20 hover:border-white/50 font-joey' ? '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' : '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 discStr = discount > 0 ? ` (${discountPercent}% rabatt)` : ''
// Summary rows
const baseRows = [ const baseRows = [
...(os === 'windows' ? [{ label: 'Licens (Windows)', cost: costs.licenseCost }] : []), ...(os === 'windows' ? [{ label: 'Licens (Windows)', cost: costs.licenseCost }] : []),
{ label: `CPU (${cpuCores} ${cpuCores === 1 ? 'kärna' : 'kärnor'})`, cost: costs.cpuCost }, { label: `CPU (${cpuCores} ${cpuCores === 1 ? 'kärna' : 'kärnor'})`, cost: costs.cpuCost },
@ -212,17 +156,10 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
<section className={`fd-section ${bgClass}`}> <section className={`fd-section ${bgClass}`}>
<div className="fd-container"> <div className="fd-container">
{/* Section heading */}
{(heading || description) && ( {(heading || description) && (
<div className="text-center mb-10 md:mb-14"> <div className="text-center mb-10 md:mb-14">
{heading && ( {heading && <h2 className={`font-joey-heavy text-fd-h1 mb-3 ${headingColor}`}>{heading}</h2>}
<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>}
)}
{description && (
<p className={`font-joey text-fd-body-lg max-w-[600px] mx-auto ${descColor}`}>
{description}
</p>
)}
{discount > 0 && ( {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"> <span className="inline-block mt-3 px-4 py-1 bg-fd-mint text-fd-navy font-joey-bold text-fd-small rounded-full">
{discountPercent}% rabatt alla resurser {discountPercent}% rabatt alla resurser
@ -233,11 +170,10 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6 lg:gap-8"> <div className="grid grid-cols-1 lg:grid-cols-5 gap-6 lg:gap-8">
{/* ── Left: Config ── */} {/* Left: Config */}
<div className="lg:col-span-3 p-7 md:p-9" style={cardStyle}> <div className={`lg:col-span-3 p-7 md:p-9 ${cardClass}`}>
{/* OS */} <p className={`font-joey-medium text-fd-small tracking-widest uppercase mb-3 ${sectionLabel}`}>
<p className={`font-joey-medium text-fd-small tracking-widest uppercase mb-3 ${sectionLabelColor}`}>
Operativsystem Operativsystem
</p> </p>
<div className="flex gap-3 mb-6"> <div className="flex gap-3 mb-6">
@ -246,16 +182,13 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
key={opt} key={opt}
type="button" type="button"
onClick={() => setOs(opt)} onClick={() => setOs(opt)}
className={`flex-1 py-3 rounded-full text-fd-body border-2 transition-all duration-200 ${ className={`flex-1 py-3 rounded-full text-fd-body border-2 transition-all duration-200 ${os === opt ? osActiveClass : osInactiveClass}`}
os === opt ? osActiveClass : osInactiveClass
}`}
> >
{opt === 'linux' ? 'Linux (gratis)' : `Windows (+${pricing.windows}kr)`} {opt === 'linux' ? 'Linux (gratis)' : `Windows (+${pricing.windows}kr)`}
</button> </button>
))} ))}
</div> </div>
{/* Resources */}
<div> <div>
<ResourceRow label="CPU-kärnor" value={cpuCores} onChange={setCpuCores} min={1} max={32} unit="st" priceLabel={`${pricing.cpuPerCore} kr/kärna${discStr}`} isDark={isDark} /> <ResourceRow label="CPU-kärnor" value={cpuCores} onChange={setCpuCores} min={1} max={32} unit="st" priceLabel={`${pricing.cpuPerCore} kr/kärna${discStr}`} isDark={isDark} />
<ResourceRow label="RAM" value={ramGb} onChange={setRamGb} min={1} max={256} unit="GB" priceLabel={`${pricing.ramPerGb} kr/GB${discStr}`} isDark={isDark} /> <ResourceRow label="RAM" value={ramGb} onChange={setRamGb} min={1} max={256} unit="GB" priceLabel={`${pricing.ramPerGb} kr/GB${discStr}`} isDark={isDark} />
@ -263,38 +196,22 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
<ResourceRow label="HDD" value={hddGb} onChange={setHddGb} min={0} max={10000} step={100} unit="GB" priceLabel={`${pricing.hddPerGb} kr/GB${discStr}`} isDark={isDark} /> <ResourceRow label="HDD" value={hddGb} onChange={setHddGb} min={0} max={10000} step={100} unit="GB" priceLabel={`${pricing.hddPerGb} kr/GB${discStr}`} isDark={isDark} />
</div> </div>
{/* Admin fee — fixed line, not customer-controlled */}
{showAdminFee && ( {showAdminFee && (
<div className="flex items-center justify-between py-4" style={{ borderBottom: `1px solid ${dividerColor}` }}> <div className={`flex items-center justify-between py-4 border-b ${dividerClass}`}>
<span className={`font-joey-bold text-fd-body ${isDark ? 'text-white' : 'text-fd-navy'}`}> <span className={`font-joey-bold text-fd-body ${summaryLabel}`}>Adminavgift</span>
Adminavgift <span className={`font-joey-medium text-fd-body ${categoryLabel}`}>{formatKr(feeAmount)}</span>
</span>
<span className={`font-joey-medium text-fd-body ${isDark ? 'text-white/70' : 'text-fd-navy/70'}`}>
{formatKr(feeAmount)}
</span>
</div> </div>
)} )}
{/* Tillvalstjänster */}
{hasTillval && ( {hasTillval && (
<div className="mt-7"> <div className="mt-7">
<p className={`font-joey-medium text-fd-small tracking-widest uppercase mb-1 ${sectionLabelColor}`}> <p className={`font-joey-medium text-fd-small tracking-widest uppercase mb-1 ${sectionLabel}`}>Tillvalstjänster</p>
Tillvalstjänster
</p>
{(additionalServices ?? []).map((svc, i) => ( {(additionalServices ?? []).map((svc, i) => (
<div <div key={i} className={`flex items-center gap-4 py-4 border-b ${dividerClass}`}>
key={i}
className="flex items-center gap-4 py-4"
style={{ borderBottom: `1px solid ${dividerColor}` }}
>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<span className={`font-joey-bold text-fd-body ${isDark ? 'text-white' : 'text-fd-navy'}`}> <span className={`font-joey-bold text-fd-body ${summaryLabel}`}>{svc.label}</span>
{svc.label}
</span>
{svc.price != null && ( {svc.price != null && (
<span className={`block font-joey text-fd-small ${isDark ? 'text-white/50' : 'text-fd-navy/50'}`}> <span className={`block font-joey text-fd-small ${categoryLabel}`}>{svc.price} kr/mån</span>
{svc.price} kr/mån
</span>
)} )}
</div> </div>
<div className="flex-shrink-0 w-14"> <div className="flex-shrink-0 w-14">
@ -306,69 +223,44 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
)} )}
</div> </div>
{/* ── Right: Summary ── */} {/* Right: Summary */}
<div className="lg:col-span-2 p-7 md:p-9 flex flex-col" style={cardStyle}> <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}`}> <h3 className={`font-joey-heavy text-fd-h2 mb-6 ${headingColor}`}>Kostnadsöversikt</h3>
Kostnadsöversikt
</h3>
{/* Base cost rows */}
<div className="flex-1 space-y-0"> <div className="flex-1 space-y-0">
{baseRows.map((row, i) => ( {baseRows.map((row, i) => (
<div <div key={i} className={`flex justify-between items-center py-2.5 border-b ${dividerClass}`}>
key={i} <span className={`font-joey text-fd-body ${summaryLabel}`}>{row.label}</span>
className="flex justify-between items-center py-2.5" <span className={`font-joey-bold text-fd-body ${summaryLabel}`}>{formatKr(row.cost)}</span>
style={{ borderBottom: `1px solid ${dividerColor}` }}
>
<span className={`font-joey text-fd-body ${summaryLabelColor}`}>{row.label}</span>
<span className={`font-joey-bold text-fd-body ${summaryValueColor}`}>
{formatKr(row.cost)}
</span>
</div> </div>
))} ))}
{/* Tillval section in summary */}
{tillvalRows.length > 0 && ( {tillvalRows.length > 0 && (
<> <>
<div className="pt-4 pb-1"> <div className="pt-4 pb-1">
<span className={`font-joey text-fd-small ${categoryLabelColor}`}>Tillval</span> <span className={`font-joey text-fd-small ${categoryLabel}`}>Tillval</span>
</div> </div>
{tillvalRows.map((row, i) => ( {tillvalRows.map((row, i) => (
<div <div key={i} className={`flex justify-between items-center py-2.5 border-b ${dividerClass}`}>
key={i} <span className={`font-joey text-fd-body ${summaryLabel}`}>{row.label}</span>
className="flex justify-between items-center py-2.5" <span className={`font-joey-bold text-fd-body ${summaryLabel}`}>{formatKr(row.cost)}</span>
style={{ borderBottom: `1px solid ${dividerColor}` }}
>
<span className={`font-joey text-fd-body ${summaryLabelColor}`}>{row.label}</span>
<span className={`font-joey-bold text-fd-body ${summaryValueColor}`}>
{formatKr(row.cost)}
</span>
</div> </div>
))} ))}
</> </>
)} )}
</div> </div>
{/* Total */} <div className={`mt-6 pt-5 border-t-2 ${dividerClass}`}>
<div className="mt-6 pt-5" style={{ borderTop: `2px solid ${dividerColor}` }}>
<div className="flex justify-between items-baseline"> <div className="flex justify-between items-baseline">
<span className={`font-joey text-fd-body ${summaryLabelColor}`}>Totalt per månad</span> <span className={`font-joey text-fd-body ${summaryLabel}`}>Totalt per månad</span>
<span className={`font-joey-heavy text-fd-h1 ${isDark ? 'text-white' : 'text-fd-navy'}`}> <span className={`font-joey-heavy text-fd-h1 ${headingColor}`}>{formatKr(costs.total)}</span>
{formatKr(costs.total)}
</span>
</div> </div>
<p className={`text-right font-joey text-fd-small mt-1 ${categoryLabelColor}`}>exkl. moms</p> <p className={`text-right font-joey text-fd-small mt-1 ${categoryLabel}`}>exkl. moms</p>
</div> </div>
{/* CTAs */}
<div className="mt-7 space-y-3"> <div className="mt-7 space-y-3">
<a href={orderCtaLink || '#'} className="fd-btn-primary w-full justify-center"> <a href={orderCtaLink || '#'} className="fd-btn-primary w-full justify-center">{orderCtaText}</a>
{orderCtaText} <a href={contactCtaLink || '#'} className={`${isDark ? 'fd-btn-secondary-dark' : 'fd-btn-secondary dark:fd-btn-secondary-dark'} w-full justify-center`}>
</a>
<a
href={contactCtaLink || '#'}
className={`${isDark ? 'fd-btn-secondary-dark' : 'fd-btn-secondary'} w-full justify-center`}
>
{contactCtaText} {contactCtaText}
</a> </a>
</div> </div>

View File

@ -35,7 +35,7 @@ import { FDCtaBannerBlockComponent } from './FDCtaBannerBlock/Component'
import { FDTestimonialBlockComponent } from './FDTestimonialBlock/Component' import { FDTestimonialBlockComponent } from './FDTestimonialBlock/Component'
import { FDTeamBlockComponent } from './FDTeamBlock/Component' import { FDTeamBlockComponent } from './FDTeamBlock/Component'
const blockComponents = { const blockComponents: Record<string, React.FC<any>> = {
formBlock: FormBlock, formBlock: FormBlock,
fdHero: FDHeroBlockComponent, fdHero: FDHeroBlockComponent,
fdCtaSideImage: FDCtaSideImageBlockComponent, fdCtaSideImage: FDCtaSideImageBlockComponent,
@ -126,7 +126,6 @@ export const RenderBlocks: React.FC<{
return ( return (
<div className={isFullWidth ? '' : 'my-16'} key={index}> <div className={isFullWidth ? '' : 'my-16'} key={index}>
{/* @ts-expect-error there may be some mismatch between the expected types here */}
<Block {...block} disableInnerContainer /> <Block {...block} disableInnerContainer />
</div> </div>
) )

File diff suppressed because one or more lines are too long