fix: FDContactFormBlock dark mode + FDButton as=button for submit

This commit is contained in:
Jeffrey 2026-02-20 11:51:58 +01:00
parent 576ecf1e97
commit fbf44b784d
10 changed files with 78 additions and 88 deletions

View File

@ -3,5 +3,5 @@ import { revalidateTag } from 'next/cache'
export const revalidateFooter: GlobalAfterChangeHook = ({ req: { payload } }) => { export const revalidateFooter: GlobalAfterChangeHook = ({ req: { payload } }) => {
payload.logger.info({ msg: 'Revalidating footer' }) payload.logger.info({ msg: 'Revalidating footer' })
;(revalidateTag as any)('global_footer') revalidateTag('global_footer', 'max')
} }

View File

@ -3,5 +3,5 @@ import { revalidateTag } from 'next/cache'
export const revalidateHeader: GlobalAfterChangeHook = ({ req: { payload } }) => { export const revalidateHeader: GlobalAfterChangeHook = ({ req: { payload } }) => {
payload.logger.info({ msg: 'Revalidating header' }) payload.logger.info({ msg: 'Revalidating header' })
;(revalidateTag as any)('global_header') revalidateTag('global_header', 'max')
} }

View File

@ -4,19 +4,21 @@ import React, { useState } from 'react'
import type { FDContactFormBlock as FDContactFormBlockProps } from '@/payload-types' import type { FDContactFormBlock as FDContactFormBlockProps } from '@/payload-types'
import type { Media } from '@/payload-types' import type { Media } from '@/payload-types'
import { FDImage } from '@/components/FDImage' import { FDImage } from '@/components/FDImage'
import { FDButton } from '@/components/FDButton'
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Theme maps */ /* Theme maps */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
const sectionBgMap: Record<string, string> = { const sectionBgMap: Record<string, string> = {
white: 'bg-white', white: 'bg-white dark:bg-fd-navy',
gray: 'bg-[#F0F0F0]', gray: 'bg-fd-gray-light dark:bg-fd-navy',
navy: 'bg-fd-navy', navy: 'bg-fd-navy',
navyGradient: 'bg-gradient-to-br from-fd-navy via-[#153350] to-fd-navy', navyGradient: 'bg-gradient-to-br from-fd-navy via-[#153350] to-fd-navy',
} }
const isDark = (bg: string) => bg === 'navy' || bg === 'navyGradient' // Navy/navyGradient are always dark. White/gray adapt via OS dark: classes.
const isExplicitDark = (bg: string) => bg === 'navy' || bg === 'navyGradient'
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Component */ /* Component */
@ -40,11 +42,10 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
const media = sideImage as Media | undefined const media = sideImage as Media | undefined
const hasSideImage = layout === 'withImage' && Boolean(media?.url) const hasSideImage = layout === 'withImage' && Boolean(media?.url)
const dark = isDark(sectionBackground || 'white') const dark = isExplicitDark(sectionBackground || 'white')
const sectionBg = sectionBgMap[sectionBackground || 'white'] const sectionBg = sectionBgMap[sectionBackground || 'white']
const isCard = layout === 'card' const isCard = layout === 'card'
// Extract form object
const form = formRelation && typeof formRelation === 'object' ? formRelation : null const form = formRelation && typeof formRelation === 'object' ? formRelation : null
const formId = form ? form.id : (formRelation ?? null) const formId = form ? form.id : (formRelation ?? null)
@ -60,26 +61,18 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
setStatus('sending') setStatus('sending')
try { try {
// 1. Submit to Payload
const res = await fetch('/api/form-submissions', { const res = await fetch('/api/form-submissions', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
form: formId, form: formId,
submissionData: Object.entries(formData).map(([field, value]) => ({ submissionData: Object.entries(formData).map(([field, value]) => ({ field, value })),
field,
value,
})),
}), }),
}) })
// 2. Submit to external API if configured
if (externalApi?.enabled && externalApi?.endpoint) { if (externalApi?.enabled && externalApi?.endpoint) {
const headers: Record<string, string> = { 'Content-Type': 'application/json' } const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (externalApi.authToken) { if (externalApi.authToken) headers['Authorization'] = `Bearer ${externalApi.authToken}`
headers['Authorization'] = `Bearer ${externalApi.authToken}`
}
// Fire and forget — don't block UX on external API
fetch(externalApi.endpoint, { fetch(externalApi.endpoint, {
method: 'POST', method: 'POST',
headers, headers,
@ -89,7 +82,7 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
data: formData, data: formData,
submittedAt: new Date().toISOString(), submittedAt: new Date().toISOString(),
}), }),
}).catch(() => {}) // silently fail }).catch(() => {})
} }
setStatus(res.ok ? 'sent' : 'error') setStatus(res.ok ? 'sent' : 'error')
@ -98,27 +91,31 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
} }
} }
/* ---- Input styles (light vs dark) ---- */ /* ---- Input styles ---- */
// Light sections: light inputs + dark: variants for OS dark mode
// Dark sections: always dark inputs
const inputBase = 'w-full rounded-xl px-4 py-3 font-joey text-base outline-none transition-all' const inputBase = 'w-full rounded-xl px-4 py-3 font-joey text-base outline-none transition-all'
const inputLight = `${inputBase} bg-gray-100 text-fd-navy placeholder:text-fd-navy/40 focus:ring-2 focus:ring-fd-navy/20 focus:bg-white`
const inputDark = `${inputBase} bg-white/10 text-white placeholder:text-white/40 border border-white/20 focus:ring-2 focus:ring-fd-yellow/30 focus:bg-white/15 focus:border-fd-yellow/50`
const inputClass = dark ? inputDark : inputLight
const checkboxLight = 'w-5 h-5 rounded border-gray-300 text-fd-navy focus:ring-fd-navy/20' const inputClass = dark
const checkboxDark = 'w-5 h-5 rounded border-white/30 text-fd-yellow focus:ring-fd-yellow/30' ? `${inputBase} bg-white/10 text-white placeholder:text-white/40 border border-white/20 focus:ring-2 focus:ring-fd-yellow/30 focus:bg-white/15 focus:border-fd-yellow/50`
const checkboxClass = dark ? checkboxDark : checkboxLight : `${inputBase} bg-gray-100 text-fd-navy placeholder:text-fd-navy/40 focus:ring-2 focus:ring-fd-navy/20 focus:bg-white dark:bg-white/10 dark:text-white dark:placeholder:text-white/40 dark:border dark:border-white/20 dark:focus:ring-fd-yellow/30 dark:focus:bg-white/15`
const checkboxClass = dark
? 'w-5 h-5 rounded border-white/30 text-fd-yellow focus:ring-fd-yellow/30'
: 'w-5 h-5 rounded border-gray-300 text-fd-navy focus:ring-fd-navy/20 dark:border-white/30 dark:text-fd-yellow dark:focus:ring-fd-yellow/30'
/* ---- Text color helpers ---- */ /* ---- Text color helpers ---- */
const headingColor = dark ? 'text-fd-yellow' : 'text-fd-navy' const headingColor = dark ? 'text-fd-yellow' : 'text-fd-navy dark:text-fd-yellow'
const bodyColor = dark ? 'text-white/80' : 'text-fd-navy/80' const bodyColor = dark ? 'text-white/80' : 'text-fd-navy/80 dark:text-white/80'
const labelColor = dark ? 'text-white' : 'text-fd-navy' const labelColor = dark ? 'text-white' : 'text-fd-navy dark:text-white'
const mutedColor = dark ? 'text-white/50' : 'text-fd-navy/60' const mutedColor = dark ? 'text-white/50' : 'text-fd-navy/60 dark:text-white/50'
const errorColor = dark ? 'text-red-300' : 'text-red-600 dark:text-red-300'
const requiredColor = dark ? 'text-fd-yellow' : 'text-fd-navy dark:text-fd-yellow'
const linkColor = dark const linkColor = dark
? 'underline underline-offset-2 hover:text-fd-yellow transition-colors' ? 'underline underline-offset-2 hover:text-fd-yellow transition-colors'
: 'underline underline-offset-2 hover:text-fd-navy transition-colors' : 'underline underline-offset-2 hover:text-fd-navy dark:hover:text-fd-yellow transition-colors'
const errorColor = dark ? 'text-red-300' : 'text-red-600'
/* ---- Field renderer ---- */ /* ---- Field renderer ---- */
@ -153,9 +150,7 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
> >
<option value="">Välj...</option> <option value="">Välj...</option>
{field.options?.map((opt: any) => ( {field.options?.map((opt: any) => (
<option key={opt.value} value={opt.value}> <option key={opt.value} value={opt.value}>{opt.label}</option>
{opt.label}
</option>
))} ))}
</select> </select>
) )
@ -214,16 +209,14 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
} }
})() })()
if (blockType === 'checkbox') { if (blockType === 'checkbox') return { element: input, width }
return { element: input, width }
}
return { return {
element: ( element: (
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<label className={`font-joey text-sm md:text-base ${labelColor}`}> <label className={`font-joey text-sm md:text-base ${labelColor}`}>
{label} {label}
{required && <span className={dark ? 'text-fd-yellow' : 'text-fd-navy'}> *</span>} {required && <span className={requiredColor}> *</span>}
</label> </label>
{input} {input}
</div> </div>
@ -232,7 +225,7 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
} }
} }
/* ---- Fields layout (handles half-width pairing) ---- */ /* ---- Fields layout ---- */
const renderFields = () => { const renderFields = () => {
if (!form?.fields) return null if (!form?.fields) return null
@ -245,9 +238,7 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
const field = fields[i] const field = fields[i]
if (field.blockType === 'message') { if (field.blockType === 'message') {
elements.push( elements.push(<div key={i} className={`font-joey text-sm ${mutedColor}`} />)
<div key={i} className={`font-joey text-sm ${mutedColor}`} />,
)
i++ i++
continue continue
} }
@ -290,8 +281,8 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
return ( return (
<section className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}> <section className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}>
<div className="max-w-[1200px] mx-auto px-6 md:px-8 text-center"> <div className="max-w-[1200px] mx-auto px-6 md:px-8 text-center">
<div className={`inline-flex items-center justify-center w-16 h-16 rounded-full mb-6 ${dark ? 'bg-fd-yellow/20' : 'bg-fd-mint/20'}`}> <div className={`inline-flex items-center justify-center w-16 h-16 rounded-full mb-6 ${dark ? 'bg-fd-yellow/20' : 'bg-fd-mint/20 dark:bg-fd-yellow/20'}`}>
<svg className={`w-8 h-8 ${dark ? 'text-fd-yellow' : 'text-fd-mint'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}> <svg className={`w-8 h-8 ${dark ? 'text-fd-yellow' : 'text-fd-mint dark:text-fd-yellow'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg> </svg>
</div> </div>
@ -321,9 +312,7 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
const formContent = ( const formContent = (
<> <>
{heading && ( {heading && (
<h2 <h2 className={`font-joey-heavy text-3xl md:text-4xl lg:text-5xl leading-tight mb-3 ${headingColor}`}>
className={`font-joey-heavy text-3xl md:text-4xl lg:text-5xl leading-tight mb-3 ${headingColor}`}
>
{heading} {heading}
</h2> </h2>
)} )}
@ -334,17 +323,18 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
<form onSubmit={handleSubmit} className="flex flex-col gap-5"> <form onSubmit={handleSubmit} className="flex flex-col gap-5">
{renderFields()} {renderFields()}
<button {/* Submit — uses FDButton, onDark when section is navy or OS is in dark mode */}
<div className="mt-2">
<FDButton
as="button"
type="submit" type="submit"
variant="primary"
onDark={dark}
disabled={status === 'sending'} disabled={status === 'sending'}
className={`self-start inline-flex items-center justify-center px-8 py-3 rounded-full font-joey-bold text-lg transition-colors disabled:opacity-60 mt-2 ${
dark
? 'bg-fd-yellow hover:bg-fd-yellow/90 text-fd-navy'
: 'bg-fd-yellow hover:bg-fd-yellow/90 text-fd-navy'
}`}
> >
{status === 'sending' ? 'Skickar...' : submitText} {status === 'sending' ? 'Skickar...' : submitText}
</button> </FDButton>
</div>
{status === 'error' && ( {status === 'error' && (
<p className={`font-joey text-sm ${errorColor}`}> <p className={`font-joey text-sm ${errorColor}`}>
@ -380,7 +370,7 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
className={`max-w-[720px] mx-auto p-8 md:p-12 rounded-[40px] md:rounded-[70px] ${ className={`max-w-[720px] mx-auto p-8 md:p-12 rounded-[40px] md:rounded-[70px] ${
dark dark
? 'bg-white/5 border border-white/10 backdrop-blur-sm' ? 'bg-white/5 border border-white/10 backdrop-blur-sm'
: 'bg-white shadow-lg border border-gray-100' : 'bg-white shadow-lg border border-gray-100 dark:bg-white/5 dark:border-white/10 dark:backdrop-blur-sm'
}`} }`}
> >
{formContent} {formContent}
@ -402,7 +392,7 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
<FDImage <FDImage
media={media!} media={media!}
size="medium" size="medium"
className="w-[380px] h-auto max-h-[560px] object-cover rounded-[20px]" className="w-[380px] h-auto max-h-[560px] object-cover rounded-[70px]"
sizes="380px" sizes="380px"
fallbackAlt={heading || ''} fallbackAlt={heading || ''}
/> />
@ -413,7 +403,7 @@ export const FDContactFormBlockComponent: React.FC<FDContactFormBlockProps> = ({
) )
} }
/* ---- Layout: Standard (full width) ---- */ /* ---- Layout: Standard ---- */
return ( return (
<section className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}> <section className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}>

View File

@ -11,15 +11,15 @@ export const revalidatePage: CollectionAfterChangeHook<Page> = ({
if (doc._status === 'published') { if (doc._status === 'published') {
const path = doc.slug === 'home' ? '/' : `/${doc.slug}` const path = doc.slug === 'home' ? '/' : `/${doc.slug}`
payload.logger.info({ msg: `Revalidating page at path: ${path}` }) payload.logger.info({ msg: `Revalidating page at path: ${path}` })
;(revalidatePath as any)(path, 'page') revalidatePath(path, 'page')
;(revalidateTag as any)('pages-sitemap') revalidateTag('pages-sitemap', 'max')
} }
if (previousDoc?._status === 'published' && doc._status !== 'published') { if (previousDoc?._status === 'published' && doc._status !== 'published') {
const oldPath = previousDoc.slug === 'home' ? '/' : `/${previousDoc.slug}` const oldPath = previousDoc.slug === 'home' ? '/' : `/${previousDoc.slug}`
payload.logger.info({ msg: `Revalidating old page at path: ${oldPath}` }) payload.logger.info({ msg: `Revalidating old page at path: ${oldPath}` })
;(revalidatePath as any)(oldPath, 'page') revalidatePath(oldPath, 'page')
;(revalidateTag as any)('pages-sitemap') revalidateTag('pages-sitemap', 'max')
} }
} }
return doc return doc
@ -28,8 +28,8 @@ export const revalidatePage: CollectionAfterChangeHook<Page> = ({
export const revalidateDelete: CollectionAfterDeleteHook<Page> = ({ doc, req: { context } }) => { export const revalidateDelete: CollectionAfterDeleteHook<Page> = ({ doc, req: { context } }) => {
if (!context.disableRevalidate) { if (!context.disableRevalidate) {
const path = doc?.slug === 'home' ? '/' : `/${doc?.slug}` const path = doc?.slug === 'home' ? '/' : `/${doc?.slug}`
;(revalidatePath as any)(path, 'page') revalidatePath(path, 'page')
;(revalidateTag as any)('pages-sitemap') revalidateTag('pages-sitemap', 'max')
} }
return doc return doc
} }

View File

@ -11,15 +11,15 @@ export const revalidatePost: CollectionAfterChangeHook<Post> = ({
if (doc._status === 'published') { if (doc._status === 'published') {
const path = `/posts/${doc.slug}` const path = `/posts/${doc.slug}`
payload.logger.info({ msg: `Revalidating post at path: ${path}` }) payload.logger.info({ msg: `Revalidating post at path: ${path}` })
;(revalidatePath as any)(path, 'page') revalidatePath(path, 'page')
;(revalidateTag as any)('posts-sitemap') revalidateTag('posts-sitemap', 'max')
} }
if (previousDoc?._status === 'published' && doc._status !== 'published') { if (previousDoc?._status === 'published' && doc._status !== 'published') {
const oldPath = `/posts/${previousDoc.slug}` const oldPath = `/posts/${previousDoc.slug}`
payload.logger.info({ msg: `Revalidating old post at path: ${oldPath}` }) payload.logger.info({ msg: `Revalidating old post at path: ${oldPath}` })
;(revalidatePath as any)(oldPath, 'page') revalidatePath(oldPath, 'page')
;(revalidateTag as any)('posts-sitemap') revalidateTag('posts-sitemap', 'max')
} }
} }
return doc return doc
@ -28,8 +28,8 @@ export const revalidatePost: CollectionAfterChangeHook<Post> = ({
export const revalidateDelete: CollectionAfterDeleteHook<Post> = ({ doc, req: { context } }) => { export const revalidateDelete: CollectionAfterDeleteHook<Post> = ({ doc, req: { context } }) => {
if (!context.disableRevalidate) { if (!context.disableRevalidate) {
const path = `/posts/${doc?.slug}` const path = `/posts/${doc?.slug}`
;(revalidatePath as any)(path, 'page') revalidatePath(path, 'page')
;(revalidateTag as any)('posts-sitemap') revalidateTag('posts-sitemap', 'max')
} }
return doc return doc
} }

View File

@ -3,5 +3,5 @@ import { revalidateTag } from 'next/cache'
export const revalidatePopup: GlobalAfterChangeHook = ({ req: { payload } }) => { export const revalidatePopup: GlobalAfterChangeHook = ({ req: { payload } }) => {
payload.logger.info({ msg: 'Revalidating popup announcement' }) payload.logger.info({ msg: 'Revalidating popup announcement' })
;(revalidateTag as any)('global_popup-announcement') revalidateTag('global_popup-announcement', 'max')
} }

View File

@ -3,5 +3,5 @@ import { revalidateTag } from 'next/cache'
export const revalidateAnnouncementBar: GlobalAfterChangeHook = ({ req: { payload } }) => { export const revalidateAnnouncementBar: GlobalAfterChangeHook = ({ req: { payload } }) => {
payload.logger.info({ msg: 'Revalidating announcement bar' }) payload.logger.info({ msg: 'Revalidating announcement bar' })
;(revalidateTag as any)('global_announcement-bar') revalidateTag('global_announcement-bar', 'max')
} }

View File

@ -3,5 +3,5 @@ import { revalidateTag } from 'next/cache'
export const revalidateSiteSettings: GlobalAfterChangeHook = ({ req: { payload } }) => { export const revalidateSiteSettings: GlobalAfterChangeHook = ({ req: { payload } }) => {
payload.logger.info({ msg: 'Revalidating site settings' }) payload.logger.info({ msg: 'Revalidating site settings' })
;(revalidateTag as any)('global_site-settings') revalidateTag('global_site-settings', 'max')
} }

View File

@ -2,5 +2,5 @@ import type { CollectionAfterChangeHook } from 'payload'
import { revalidateTag } from 'next/cache' import { revalidateTag } from 'next/cache'
export const revalidateRedirects: CollectionAfterChangeHook = () => { export const revalidateRedirects: CollectionAfterChangeHook = () => {
;(revalidateTag as any)('redirects') revalidateTag('redirects', 'max')
} }

File diff suppressed because one or more lines are too long