feat: OS-based dark mode across all blocks
This commit is contained in:
parent
b36f7ebe6e
commit
0bf56a1388
@ -13,7 +13,6 @@ interface HeaderClientProps {
|
||||
data: Header
|
||||
}
|
||||
|
||||
/** Resolves the logo href from the logoLink group field */
|
||||
function resolveLogoHref(logoLink: Header['logoLink']): string {
|
||||
if (!logoLink) return '/'
|
||||
if (logoLink.type === 'reference') {
|
||||
@ -28,11 +27,19 @@ function resolveLogoHref(logoLink: Header['logoLink']): string {
|
||||
}
|
||||
|
||||
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 [isDark, setIsDark] = useState(false)
|
||||
const { headerTheme, setHeaderTheme } = useHeaderTheme()
|
||||
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(() => {
|
||||
setHeaderTheme(null)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -47,12 +54,12 @@ export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
|
||||
|
||||
return (
|
||||
<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 } : {})}
|
||||
>
|
||||
<div className="container py-5 flex justify-between items-center">
|
||||
<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>
|
||||
<HeaderNav data={data} />
|
||||
</div>
|
||||
|
||||
@ -5,11 +5,9 @@ import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { MenuIcon, XIcon, ChevronDownIcon, ChevronRightIcon } from 'lucide-react'
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
type NavChild = NonNullable<NonNullable<NonNullable<HeaderType['navItems']>[number]['children']>[number]>
|
||||
type NavItem = NonNullable<HeaderType['navItems']>[number]
|
||||
|
||||
// ─── Resolve href from reference or custom url ────────────────────────────────
|
||||
function resolveHref(item: {
|
||||
type?: string | null
|
||||
url?: string | null
|
||||
@ -25,7 +23,6 @@ function resolveHref(item: {
|
||||
return item.url || '#'
|
||||
}
|
||||
|
||||
// ─── Swedish Flag ─────────────────────────────────────────────────────────────
|
||||
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]">
|
||||
<rect width="28" height="20" fill="#006AA7" />
|
||||
@ -34,7 +31,6 @@ const SwedishFlag = () => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
// ─── Group children into columns ──────────────────────────────────────────────
|
||||
function groupChildren(children: NavChild[]): { heading: string | null; links: NavChild[] }[] {
|
||||
const main: NavChild[] = []
|
||||
const groups: Record<string, NavChild[]> = {}
|
||||
@ -58,7 +54,6 @@ function groupChildren(children: NavChild[]): { heading: string | null; links: N
|
||||
return result
|
||||
}
|
||||
|
||||
// ─── Mega Menu Panel ──────────────────────────────────────────────────────────
|
||||
const MegaMenuPanel: React.FC<{
|
||||
item: NavItem
|
||||
headerRef: React.RefObject<HTMLElement | null>
|
||||
@ -94,18 +89,18 @@ const MegaMenuPanel: React.FC<{
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Panel */}
|
||||
{/* Panel — white in light, navy in dark */}
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="fixed left-0 right-0 z-40"
|
||||
style={{ top, backgroundColor: '#ffffff' }}
|
||||
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 }}
|
||||
>
|
||||
<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">
|
||||
|
||||
{/* Left: category title */}
|
||||
<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}
|
||||
</h2>
|
||||
</div>
|
||||
@ -115,7 +110,7 @@ const MegaMenuPanel: React.FC<{
|
||||
{columns.map((col, ci) => (
|
||||
<div key={ci}>
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
@ -125,10 +120,10 @@ const MegaMenuPanel: React.FC<{
|
||||
<Link
|
||||
href={resolveHref(link)}
|
||||
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
|
||||
? 'font-joey text-fd-body text-fd-navy/75 hover:text-fd-navy'
|
||||
: 'font-joey-bold text-fd-h3 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 dark:text-white hover:text-fd-yellow dark:hover:text-fd-yellow'
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
@ -147,7 +142,6 @@ const MegaMenuPanel: React.FC<{
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Main Nav ─────────────────────────────────────────────────────────────────
|
||||
export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
||||
const navItems = (data?.navItems || []) as NavItem[]
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
@ -208,7 +202,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ── Desktop nav ──────────────────────────────────────────────────── */}
|
||||
{/* ── Desktop nav ── */}
|
||||
<nav ref={navRef} className="hidden md:flex gap-6 items-center">
|
||||
{navItems.map((item, i) => {
|
||||
const hasChildren = item.children && item.children.length > 0
|
||||
@ -220,7 +214,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
||||
<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 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-expanded={isOpen}
|
||||
@ -231,10 +225,10 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Small card dropdown (non-mega only) */}
|
||||
{/* Small card dropdown */}
|
||||
{!isMega && (
|
||||
<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
|
||||
? 'opacity-100 scale-y-100 pointer-events-auto'
|
||||
: 'opacity-0 scale-y-95 pointer-events-none'
|
||||
@ -245,7 +239,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
||||
key={j}
|
||||
href={resolveHref(child)}
|
||||
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}
|
||||
</Link>
|
||||
@ -257,7 +251,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
||||
<Link
|
||||
key={i}
|
||||
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}
|
||||
</Link>
|
||||
@ -267,7 +261,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
||||
<SwedishFlag />
|
||||
</nav>
|
||||
|
||||
{/* ── Mega menu panel ───────────────────────────────────────────────── */}
|
||||
{/* ── Mega menu panel ── */}
|
||||
{activeItem?.megaMenu && activeItem.children?.length && (
|
||||
<MegaMenuPanel
|
||||
item={activeItem}
|
||||
@ -277,16 +271,16 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── Mobile hamburger ─────────────────────────────────────────────── */}
|
||||
{/* ── Mobile hamburger ── */}
|
||||
<button
|
||||
className="md:hidden text-fd-navy p-2"
|
||||
className="md:hidden text-fd-navy dark:text-white p-2"
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
aria-label={mobileOpen ? 'Stäng meny' : 'Öppna meny'}
|
||||
>
|
||||
{mobileOpen ? <XIcon className="w-6 h-6" /> : <MenuIcon className="w-6 h-6" />}
|
||||
</button>
|
||||
|
||||
{/* ── Mobile full-screen overlay ───────────────────────────────────── */}
|
||||
{/* ── Mobile full-screen overlay — always navy, no dark: needed ── */}
|
||||
<div
|
||||
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'
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
@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 md (@media (width >= theme(--breakpoint-md)));
|
||||
@custom-variant lg (@media (width >= theme(--breakpoint-lg)));
|
||||
@ -225,7 +225,8 @@ html {
|
||||
}
|
||||
|
||||
html[data-theme='dark'],
|
||||
html[data-theme='light'] {
|
||||
html[data-theme='light'],
|
||||
html:not([data-theme]) {
|
||||
opacity: initial;
|
||||
}
|
||||
|
||||
@ -719,6 +720,27 @@ html[data-theme='light'] {
|
||||
--fd-border-strong: #245580;
|
||||
}
|
||||
}
|
||||
/* ── Button dark mode overrides ───────────────────────────────────────────
|
||||
fd-btn-primary hover: navy→white (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) ────────────────────────────────
|
||||
Add these to your existing globals.css
|
||||
─────────────────────────────────────────────────────────────────────── */
|
||||
@ -728,6 +750,30 @@ html[data-theme='light'] {
|
||||
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 h2,
|
||||
.fd-prose h3,
|
||||
|
||||
@ -43,13 +43,14 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
className={cn(GeistSans.variable, GeistMono.variable)}
|
||||
lang="sv"
|
||||
suppressHydrationWarning
|
||||
style={{ colorScheme: 'light' }}
|
||||
style={{ colorScheme: 'light dark' }}
|
||||
>
|
||||
<head>
|
||||
<InitTheme />
|
||||
<link href="/favicon.ico" rel="icon" sizes="32x32" />
|
||||
<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" />
|
||||
<HeadInjection siteSettings={siteSettings} />
|
||||
</head>
|
||||
|
||||
@ -31,33 +31,28 @@ export default async function PostPage({ params }: Args) {
|
||||
const summary = post.meta?.description ?? null
|
||||
|
||||
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-[640px]">
|
||||
|
||||
{/* Back link */}
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
|
||||
{/* Title */}
|
||||
<h1 className="font-joey-heavy text-[2rem] md:text-[2.5rem] lg:text-[2.875rem] leading-[1.1] text-fd-navy mb-4">
|
||||
<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">
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
{/* Summary / excerpt (from meta description) */}
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Hero image */}
|
||||
{heroImage?.url && (
|
||||
<figure className="mb-6">
|
||||
<div className="rounded-[20px] overflow-hidden">
|
||||
@ -69,26 +64,24 @@ export default async function PostPage({ params }: Args) {
|
||||
/>
|
||||
</div>
|
||||
{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}
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
)}
|
||||
|
||||
{/* Author + date */}
|
||||
<div className="mb-8 font-joey text-sm">
|
||||
{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(', ')}
|
||||
</p>
|
||||
)}
|
||||
{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>
|
||||
|
||||
{/* Rich text body */}
|
||||
{post.content && (
|
||||
<div className="mb-12">
|
||||
<RichText
|
||||
@ -99,7 +92,6 @@ export default async function PostPage({ params }: Args) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CTA */}
|
||||
<a href="/kontakt" className="fd-btn-primary">
|
||||
Kontakta oss
|
||||
</a>
|
||||
@ -110,7 +102,6 @@ export default async function PostPage({ params }: Args) {
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Metadata ─────────────────────────────────────────────────────────────────
|
||||
export async function generateMetadata({ params }: Args): Promise<Metadata> {
|
||||
const { slug } = await params
|
||||
const payload = await getPayload({ config })
|
||||
@ -128,7 +119,6 @@ export async function generateMetadata({ params }: Args): Promise<Metadata> {
|
||||
return generateMeta({ doc: post })
|
||||
}
|
||||
|
||||
// ─── Static params ────────────────────────────────────────────────────────────
|
||||
export async function generateStaticParams() {
|
||||
const payload = await getPayload({ config })
|
||||
const posts = await payload.find({
|
||||
|
||||
@ -28,19 +28,17 @@ export default async function Page() {
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<div className="min-h-screen bg-white dark:bg-fd-navy">
|
||||
<PageClient />
|
||||
|
||||
<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 mb-10 md:mb-14">
|
||||
<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">
|
||||
Nyheter!
|
||||
</h1>
|
||||
|
||||
{/* Grid */}
|
||||
{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">
|
||||
{posts.docs.map((post) => {
|
||||
@ -53,20 +51,17 @@ export default async function Page() {
|
||||
href={`/posts/${p.slug}`}
|
||||
className="group flex flex-col"
|
||||
>
|
||||
{/* Title above image */}
|
||||
<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">
|
||||
<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">
|
||||
{p.title}
|
||||
</h2>
|
||||
|
||||
{/* Date between title and image */}
|
||||
{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)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Image with rounded corners */}
|
||||
<div className="relative aspect-[4/3] rounded-[20px] overflow-hidden bg-fd-navy/5">
|
||||
<div className="relative aspect-[4/3] rounded-[20px] overflow-hidden bg-fd-navy/5 dark:bg-white/5">
|
||||
{heroImage?.url ? (
|
||||
<FDImage
|
||||
media={heroImage}
|
||||
@ -78,7 +73,7 @@ export default async function Page() {
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
@ -88,7 +83,6 @@ export default async function Page() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{posts.totalPages > 1 && posts.page && (
|
||||
<div className="mt-16">
|
||||
<Pagination page={posts.page} totalPages={posts.totalPages} />
|
||||
|
||||
BIN
src/blocks.zip
BIN
src/blocks.zip
Binary file not shown.
@ -13,10 +13,10 @@ const cardStyleMap: Record<
|
||||
border: '',
|
||||
},
|
||||
gray: {
|
||||
bg: 'bg-[#e5e5e5]',
|
||||
headingText: 'text-fd-navy',
|
||||
bodyText: 'text-fd-navy',
|
||||
linkText: 'text-fd-navy hover:text-fd-navy/70',
|
||||
bg: 'bg-[#e5e5e5] dark:bg-white/10',
|
||||
headingText: 'text-fd-navy dark:text-fd-yellow',
|
||||
bodyText: 'text-fd-navy dark:text-white',
|
||||
linkText: 'text-fd-navy hover:text-fd-navy/70 dark:text-fd-yellow dark:hover:text-fd-yellow/80',
|
||||
border: '',
|
||||
},
|
||||
yellow: {
|
||||
@ -34,18 +34,18 @@ const cardStyleMap: Record<
|
||||
border: '',
|
||||
},
|
||||
outlined: {
|
||||
bg: 'bg-white',
|
||||
headingText: 'text-fd-navy',
|
||||
bodyText: 'text-fd-navy',
|
||||
linkText: 'text-fd-navy/70 hover:text-fd-navy',
|
||||
border: 'border-6 border-gray-200 shadow-sm',
|
||||
bg: 'bg-white dark:bg-white/10',
|
||||
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',
|
||||
},
|
||||
}
|
||||
|
||||
const sectionBgMap: Record<string, string> = {
|
||||
white: 'bg-white',
|
||||
white: 'bg-white dark: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> = {
|
||||
|
||||
@ -29,8 +29,8 @@ export const FDHeroBlockComponent: React.FC<FDHeroBlockProps> = ({
|
||||
headingColor = 'text-fd-navy'
|
||||
textBodyColor = 'text-fd-navy'
|
||||
} else {
|
||||
headingColor = isDark ? 'text-fd-yellow' : 'text-fd-navy'
|
||||
textBodyColor = isDark ? 'text-white' : 'text-fd-navy'
|
||||
headingColor = isDark ? 'text-fd-yellow' : 'text-fd-navy dark:text-fd-yellow'
|
||||
textBodyColor = isDark ? 'text-white' : 'text-fd-navy dark:text-white'
|
||||
}
|
||||
|
||||
const overlayClass =
|
||||
@ -40,7 +40,9 @@ export const FDHeroBlockComponent: React.FC<FDHeroBlockProps> = ({
|
||||
|
||||
return (
|
||||
<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 && (
|
||||
<>
|
||||
|
||||
@ -13,10 +13,10 @@ export const FDServiceChooserBlockComponent: React.FC<Props> = ({
|
||||
const prevIndex = useRef(0)
|
||||
|
||||
const isDark = sectionBackground === 'navy'
|
||||
const bgClass = isDark ? 'bg-fd-navy' : sectionBackground === 'gray' ? 'bg-fd-surface-alt' : 'bg-white'
|
||||
const titleClass = isDark ? 'text-fd-yellow' : 'text-fd-navy'
|
||||
const bodyClass = isDark ? 'text-white' : 'text-fd-navy'
|
||||
const cardBg = isDark ? 'bg-white/10 border border-white/10' : 'bg-white border border-fd-navy/10'
|
||||
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 handleTabChange = (i: number) => {
|
||||
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'
|
||||
: isDark
|
||||
? '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}
|
||||
@ -87,7 +87,7 @@ export const FDServiceChooserBlockComponent: React.FC<Props> = ({
|
||||
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}
|
||||
</h3>
|
||||
{service.description && (
|
||||
@ -99,7 +99,7 @@ export const FDServiceChooserBlockComponent: React.FC<Props> = ({
|
||||
<div className="mt-auto pt-2">
|
||||
<a
|
||||
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}
|
||||
</a>
|
||||
|
||||
@ -12,104 +12,59 @@ const DEFAULT_PRICING = {
|
||||
adminFee: 200,
|
||||
}
|
||||
|
||||
const formatKr = (n: number) => {
|
||||
// Format like "2 495 kr" with space as thousands separator
|
||||
return Math.round(n).toLocaleString('sv-SE') + ' kr'
|
||||
}
|
||||
const formatKr = (n: number) => Math.round(n).toLocaleString('sv-SE') + ' kr'
|
||||
|
||||
// ─── Toggle switch ───────────────────────────────────────────────────────────
|
||||
function Toggle({ active, onToggle }: { active: boolean; onToggle: () => void }) {
|
||||
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]'
|
||||
}`}
|
||||
className={`relative rounded-full transition-all duration-300 ${active ? 'bg-fd-yellow' : 'bg-[#e2e8f0] dark:bg-white/20'}`}
|
||||
role="switch"
|
||||
aria-checked={active}
|
||||
>
|
||||
<div
|
||||
className={`absolute top-1 w-6 h-6 rounded-full bg-white shadow-sm transition-all duration-300 ${
|
||||
active ? 'left-7' : 'left-1'
|
||||
}`}
|
||||
/>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Number stepper row ──────────────────────────────────────────────────────
|
||||
function ResourceRow({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
min = 0,
|
||||
max = 999,
|
||||
step = 1,
|
||||
unit,
|
||||
priceLabel,
|
||||
isDark,
|
||||
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
|
||||
label: string; value: number; 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 (
|
||||
<div
|
||||
className="flex items-center justify-between py-4"
|
||||
style={{ borderBottom: `1px solid ${borderColor}` }}
|
||||
>
|
||||
<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 ${labelColor}`}>{label}</span>
|
||||
<span className={`font-joey text-fd-small ${subColor}`}>{priceLabel}</span>
|
||||
<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>
|
||||
</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 ${btnColor}`}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
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>
|
||||
<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 ${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' }}
|
||||
/>
|
||||
<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
|
||||
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 ${btnColor}`}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Main component ──────────────────────────────────────────────────────────
|
||||
export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps> = ({
|
||||
heading = 'Virtuell server — kalkylator',
|
||||
description,
|
||||
@ -153,46 +108,35 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
|
||||
const ramCost = disc(ramGb * pricing.ramPerGb)
|
||||
const ssdCost = disc(ssdGb * pricing.ssdPerGb)
|
||||
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 extraCosts = (additionalServices ?? []).map((svc, i) =>
|
||||
extraToggles[i] ? (svc.price ?? 0) : 0
|
||||
)
|
||||
const extraCosts = (additionalServices ?? []).map((svc, i) => extraToggles[i] ? (svc.price ?? 0) : 0)
|
||||
const extraTotal = extraCosts.reduce((a, b) => a + b, 0)
|
||||
const total = licenseCost + cpuCost + ramCost + ssdCost + hddCost + feeCost + extraTotal
|
||||
return { licenseCost, cpuCost, ramCost, ssdCost, hddCost, feeCost, extraCosts, total }
|
||||
}, [os, cpuCores, ramGb, ssdGb, hddGb, extraToggles, pricing, discount, feeAmount, showAdminFee, additionalServices])
|
||||
|
||||
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
|
||||
const cardBg = isDark ? 'rgba(255,255,255,0.05)' : '#ffffff'
|
||||
const cardBorder = isDark ? 'rgba(255,255,255,0.12)' : '#e2e8f0'
|
||||
const cardStyle: React.CSSProperties = {
|
||||
background: cardBg,
|
||||
border: `6px solid ${cardBorder}`,
|
||||
borderRadius: 'clamp(28px, 4vw, 60px)',
|
||||
}
|
||||
// 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'
|
||||
|
||||
// Colors
|
||||
const headingColor = isDark ? 'text-white' : 'text-fd-navy'
|
||||
const descColor = isDark ? 'text-white/60' : 'text-fd-navy/60'
|
||||
const sectionLabelColor = isDark ? 'text-white/40' : 'text-fd-navy/35'
|
||||
const dividerColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(14,35,56,0.08)'
|
||||
const summaryLabelColor = isDark ? 'text-white' : 'text-fd-navy'
|
||||
const summaryValueColor = isDark ? 'text-white' : 'text-fd-navy'
|
||||
const categoryLabelColor = isDark ? 'text-white/40' : 'text-fd-navy/40'
|
||||
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'
|
||||
|
||||
// OS toggle
|
||||
const osActiveClass = 'bg-fd-yellow text-fd-navy border-fd-yellow font-joey-bold'
|
||||
const osInactiveClass = 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'
|
||||
: '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)` : ''
|
||||
|
||||
// Summary rows
|
||||
const baseRows = [
|
||||
...(os === 'windows' ? [{ label: 'Licens (Windows)', cost: costs.licenseCost }] : []),
|
||||
{ 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}`}>
|
||||
<div className="fd-container">
|
||||
|
||||
{/* Section heading */}
|
||||
{(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>
|
||||
)}
|
||||
{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">
|
||||
{discountPercent}% rabatt på 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">
|
||||
|
||||
{/* ── Left: Config ── */}
|
||||
<div className="lg:col-span-3 p-7 md:p-9" style={cardStyle}>
|
||||
{/* Left: Config */}
|
||||
<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 ${sectionLabelColor}`}>
|
||||
<p className={`font-joey-medium text-fd-small tracking-widest uppercase mb-3 ${sectionLabel}`}>
|
||||
Operativsystem
|
||||
</p>
|
||||
<div className="flex gap-3 mb-6">
|
||||
@ -246,16 +182,13 @@ 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 border-2 transition-all duration-200 ${os === opt ? osActiveClass : osInactiveClass}`}
|
||||
>
|
||||
{opt === 'linux' ? 'Linux (gratis)' : `Windows (+${pricing.windows}kr)`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Resources */}
|
||||
<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="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} />
|
||||
</div>
|
||||
|
||||
{/* Admin fee — fixed line, not customer-controlled */}
|
||||
{showAdminFee && (
|
||||
<div className="flex items-center justify-between py-4" style={{ borderBottom: `1px solid ${dividerColor}` }}>
|
||||
<span className={`font-joey-bold text-fd-body ${isDark ? 'text-white' : 'text-fd-navy'}`}>
|
||||
Adminavgift
|
||||
</span>
|
||||
<span className={`font-joey-medium text-fd-body ${isDark ? 'text-white/70' : 'text-fd-navy/70'}`}>
|
||||
{formatKr(feeAmount)}
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tillvalstjänster */}
|
||||
{hasTillval && (
|
||||
<div className="mt-7">
|
||||
<p className={`font-joey-medium text-fd-small tracking-widest uppercase mb-1 ${sectionLabelColor}`}>
|
||||
Tillvalstjänster
|
||||
</p>
|
||||
<p className={`font-joey-medium text-fd-small tracking-widest uppercase mb-1 ${sectionLabel}`}>Tillvalstjänster</p>
|
||||
{(additionalServices ?? []).map((svc, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-4 py-4"
|
||||
style={{ borderBottom: `1px solid ${dividerColor}` }}
|
||||
>
|
||||
<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 ${isDark ? 'text-white' : 'text-fd-navy'}`}>
|
||||
{svc.label}
|
||||
</span>
|
||||
<span className={`font-joey-bold text-fd-body ${summaryLabel}`}>{svc.label}</span>
|
||||
{svc.price != null && (
|
||||
<span className={`block font-joey text-fd-small ${isDark ? 'text-white/50' : 'text-fd-navy/50'}`}>
|
||||
{svc.price} kr/mån
|
||||
</span>
|
||||
<span className={`block font-joey text-fd-small ${categoryLabel}`}>{svc.price} kr/mån</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-shrink-0 w-14">
|
||||
@ -306,69 +223,44 @@ export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Right: Summary ── */}
|
||||
<div className="lg:col-span-2 p-7 md:p-9 flex flex-col" style={cardStyle}>
|
||||
<h3 className={`font-joey-heavy text-fd-h2 mb-6 ${headingColor}`}>
|
||||
Kostnadsöversikt
|
||||
</h3>
|
||||
{/* 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}`}>Kostnadsöversikt</h3>
|
||||
|
||||
{/* Base cost rows */}
|
||||
<div className="flex-1 space-y-0">
|
||||
{baseRows.map((row, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex justify-between items-center py-2.5"
|
||||
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 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>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Tillval section in summary */}
|
||||
{tillvalRows.length > 0 && (
|
||||
<>
|
||||
<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>
|
||||
{tillvalRows.map((row, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex justify-between items-center py-2.5"
|
||||
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 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>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Total */}
|
||||
<div className="mt-6 pt-5" style={{ borderTop: `2px solid ${dividerColor}` }}>
|
||||
<div className={`mt-6 pt-5 border-t-2 ${dividerClass}`}>
|
||||
<div className="flex justify-between items-baseline">
|
||||
<span className={`font-joey text-fd-body ${summaryLabelColor}`}>Totalt per månad</span>
|
||||
<span className={`font-joey-heavy text-fd-h1 ${isDark ? 'text-white' : 'text-fd-navy'}`}>
|
||||
{formatKr(costs.total)}
|
||||
</span>
|
||||
<span className={`font-joey text-fd-body ${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 ${categoryLabelColor}`}>exkl. moms</p>
|
||||
<p className={`text-right font-joey text-fd-small mt-1 ${categoryLabel}`}>exkl. moms</p>
|
||||
</div>
|
||||
|
||||
{/* CTAs */}
|
||||
<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'} w-full justify-center`}
|
||||
>
|
||||
<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>
|
||||
|
||||
@ -35,7 +35,7 @@ import { FDCtaBannerBlockComponent } from './FDCtaBannerBlock/Component'
|
||||
import { FDTestimonialBlockComponent } from './FDTestimonialBlock/Component'
|
||||
import { FDTeamBlockComponent } from './FDTeamBlock/Component'
|
||||
|
||||
const blockComponents = {
|
||||
const blockComponents: Record<string, React.FC<any>> = {
|
||||
formBlock: FormBlock,
|
||||
fdHero: FDHeroBlockComponent,
|
||||
fdCtaSideImage: FDCtaSideImageBlockComponent,
|
||||
@ -126,7 +126,6 @@ export const RenderBlocks: React.FC<{
|
||||
|
||||
return (
|
||||
<div className={isFullWidth ? '' : 'my-16'} key={index}>
|
||||
{/* @ts-expect-error there may be some mismatch between the expected types here */}
|
||||
<Block {...block} disableInnerContainer />
|
||||
</div>
|
||||
)
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user