feat: 3 new blocks FDCtaBannerBlock, FDTestimonialBlock, FDTeamBlock
This commit is contained in:
parent
0712c0cc9d
commit
cbeae1bf85
BIN
media/Screenshot 2026-02-18 at 13.41.09.png
Normal file
BIN
media/Screenshot 2026-02-18 at 13.41.09.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
97
src/blocks/FDCtaBannerBlock/Component.tsx
Normal file
97
src/blocks/FDCtaBannerBlock/Component.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import type { FDCtaBannerBlock as FDCtaBannerBlockProps } from '@/payload-types'
|
||||||
|
|
||||||
|
const bgMap: Record<string, { section: string; heading: string; sub: string; primaryBtn: string; secondaryBtn: string }> = {
|
||||||
|
yellow: {
|
||||||
|
section: 'bg-fd-yellow',
|
||||||
|
heading: 'text-fd-navy',
|
||||||
|
sub: 'text-fd-navy/70',
|
||||||
|
primaryBtn: 'bg-fd-navy hover:bg-fd-navy/90 text-white',
|
||||||
|
secondaryBtn: 'border-2 border-fd-navy text-fd-navy hover:bg-fd-navy/10',
|
||||||
|
},
|
||||||
|
navy: {
|
||||||
|
section: 'bg-fd-navy',
|
||||||
|
heading: 'text-fd-yellow',
|
||||||
|
sub: 'text-white/70',
|
||||||
|
primaryBtn: 'bg-fd-yellow hover:bg-fd-yellow/90 text-fd-navy',
|
||||||
|
secondaryBtn: 'border-2 border-white text-white hover:bg-white/10',
|
||||||
|
},
|
||||||
|
gray: {
|
||||||
|
section: 'bg-[#e5e5e5]',
|
||||||
|
heading: 'text-fd-navy',
|
||||||
|
sub: 'text-fd-navy/70',
|
||||||
|
primaryBtn: 'bg-fd-navy hover:bg-fd-navy/90 text-white',
|
||||||
|
secondaryBtn: 'border-2 border-fd-navy text-fd-navy hover:bg-fd-navy/10',
|
||||||
|
},
|
||||||
|
white: {
|
||||||
|
section: 'bg-white',
|
||||||
|
heading: 'text-fd-navy',
|
||||||
|
sub: 'text-fd-navy/70',
|
||||||
|
primaryBtn: 'bg-fd-navy hover:bg-fd-navy/90 text-white',
|
||||||
|
secondaryBtn: 'border-2 border-fd-navy text-fd-navy hover:bg-fd-navy/10',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeMap: Record<string, { py: string; heading: string; sub: string }> = {
|
||||||
|
small: { py: 'py-10 md:py-14', heading: 'text-2xl md:text-3xl lg:text-4xl', sub: 'text-base md:text-lg' },
|
||||||
|
medium: { py: 'py-14 md:py-20 lg:py-[80px]', heading: 'text-3xl md:text-4xl lg:text-5xl', sub: 'text-lg md:text-xl' },
|
||||||
|
large: { py: 'py-16 md:py-24 lg:py-[99px]', heading: 'text-4xl md:text-5xl lg:text-[64px]', sub: 'text-xl md:text-2xl' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FDCtaBannerBlockComponent: React.FC<FDCtaBannerBlockProps> = ({
|
||||||
|
heading,
|
||||||
|
subheading,
|
||||||
|
ctaText,
|
||||||
|
ctaLink = '/kontakt',
|
||||||
|
secondaryCtaText,
|
||||||
|
secondaryCtaLink,
|
||||||
|
sectionBackground = 'yellow',
|
||||||
|
alignment = 'center',
|
||||||
|
size = 'medium',
|
||||||
|
}) => {
|
||||||
|
const theme = bgMap[sectionBackground] || bgMap.yellow
|
||||||
|
const sizing = sizeMap[size] || sizeMap.medium
|
||||||
|
const isCenter = alignment === 'center'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={`w-full ${sizing.py} ${theme.section}`}>
|
||||||
|
<div className="max-w-[1200px] mx-auto px-6 md:px-8">
|
||||||
|
<div className={`flex flex-col gap-6 md:gap-8 ${isCenter ? 'items-center text-center' : 'items-start text-left'} max-w-[800px] ${isCenter ? 'mx-auto' : ''}`}>
|
||||||
|
|
||||||
|
<div className={`flex flex-col gap-3 ${isCenter ? 'items-center' : 'items-start'}`}>
|
||||||
|
<h2 className={`font-joey-heavy leading-tight ${sizing.heading} ${theme.heading}`}>
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
{subheading && (
|
||||||
|
<p className={`font-joey leading-relaxed ${sizing.sub} ${theme.sub}`}>
|
||||||
|
{subheading}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(ctaText || secondaryCtaText) && (
|
||||||
|
<div className={`flex flex-col sm:flex-row gap-4 ${isCenter ? 'justify-center' : ''}`}>
|
||||||
|
{ctaText && (
|
||||||
|
<a
|
||||||
|
href={ctaLink || '#'}
|
||||||
|
className={`inline-flex items-center justify-center px-8 py-3 rounded-full font-joey-bold text-lg transition-colors ${theme.primaryBtn}`}
|
||||||
|
>
|
||||||
|
{ctaText}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{secondaryCtaText && secondaryCtaLink && (
|
||||||
|
<a
|
||||||
|
href={secondaryCtaLink}
|
||||||
|
className={`inline-flex items-center justify-center px-8 py-3 rounded-full font-joey-bold text-lg transition-colors ${theme.secondaryBtn}`}
|
||||||
|
>
|
||||||
|
{secondaryCtaText}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
86
src/blocks/FDCtaBannerBlock/config.ts
Normal file
86
src/blocks/FDCtaBannerBlock/config.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const FDCtaBannerBlock: Block = {
|
||||||
|
slug: 'fdCtaBanner',
|
||||||
|
interfaceName: 'FDCtaBannerBlock',
|
||||||
|
labels: {
|
||||||
|
singular: 'FD CTA-banner',
|
||||||
|
plural: 'FD CTA-banners',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
label: 'Rubrik',
|
||||||
|
defaultValue: 'Redo att komma igång?',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subheading',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
label: 'Underrubrik (valfri)',
|
||||||
|
admin: {
|
||||||
|
description: 'Kort text under rubriken',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ctaText',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
label: 'CTA-knapp text',
|
||||||
|
defaultValue: 'Kontakta oss',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ctaLink',
|
||||||
|
type: 'text',
|
||||||
|
label: 'CTA-knapp länk',
|
||||||
|
defaultValue: '/kontakt',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'secondaryCtaText',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
label: 'Sekundär CTA text (valfri)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'secondaryCtaLink',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Sekundär CTA länk',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sectionBackground',
|
||||||
|
type: 'select',
|
||||||
|
label: 'Bakgrundsfärg',
|
||||||
|
defaultValue: 'yellow',
|
||||||
|
options: [
|
||||||
|
{ label: 'Gul', value: 'yellow' },
|
||||||
|
{ label: 'Navy', value: 'navy' },
|
||||||
|
{ label: 'Grå', value: 'gray' },
|
||||||
|
{ label: 'Vit', value: 'white' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'alignment',
|
||||||
|
type: 'select',
|
||||||
|
label: 'Textjustering',
|
||||||
|
defaultValue: 'center',
|
||||||
|
options: [
|
||||||
|
{ label: 'Centrerad', value: 'center' },
|
||||||
|
{ label: 'Vänster', value: 'left' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'size',
|
||||||
|
type: 'select',
|
||||||
|
label: 'Storlek',
|
||||||
|
defaultValue: 'medium',
|
||||||
|
options: [
|
||||||
|
{ label: 'Liten', value: 'small' },
|
||||||
|
{ label: 'Medium', value: 'medium' },
|
||||||
|
{ label: 'Stor', value: 'large' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
136
src/blocks/FDTeamBlock/Component.tsx
Normal file
136
src/blocks/FDTeamBlock/Component.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import type { FDTeamBlock as FDTeamBlockProps, Media } from '@/payload-types'
|
||||||
|
import { FDImage } from '@/components/FDImage'
|
||||||
|
|
||||||
|
const sectionBgMap: Record<string, string> = {
|
||||||
|
white: 'bg-white',
|
||||||
|
gray: 'bg-[#e5e5e5]',
|
||||||
|
navy: 'bg-fd-navy',
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardMap: Record<string, { bg: string; name: string; role: string; bio: string; icon: string }> = {
|
||||||
|
navy: { bg: 'bg-fd-navy', name: 'text-fd-yellow', role: 'text-white/70', bio: 'text-white/60', icon: 'text-white/40 hover:text-fd-yellow' },
|
||||||
|
white: { bg: 'bg-white', name: 'text-fd-navy', role: 'text-fd-navy/60', bio: 'text-fd-navy/60', icon: 'text-fd-navy/40 hover:text-fd-navy' },
|
||||||
|
gray: { bg: 'bg-[#e5e5e5]', name: 'text-fd-navy', role: 'text-fd-navy/60', bio: 'text-fd-navy/60', icon: 'text-fd-navy/40 hover:text-fd-navy' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const colsMap: Record<string, string> = {
|
||||||
|
'2': 'sm:grid-cols-2',
|
||||||
|
'3': 'sm:grid-cols-2 lg:grid-cols-3',
|
||||||
|
'4': 'sm:grid-cols-2 lg:grid-cols-4',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FDTeamBlockComponent: React.FC<FDTeamBlockProps> = ({
|
||||||
|
heading,
|
||||||
|
subheading,
|
||||||
|
members,
|
||||||
|
columns = '3',
|
||||||
|
cardStyle = 'navy',
|
||||||
|
sectionBackground = 'white',
|
||||||
|
}) => {
|
||||||
|
const sectionBg = sectionBgMap[sectionBackground] || sectionBgMap.white
|
||||||
|
const card = cardMap[cardStyle] || cardMap.navy
|
||||||
|
const gridCols = colsMap[columns] || colsMap['3']
|
||||||
|
const isNavySection = sectionBackground === 'navy'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={`w-full py-16 md:py-20 lg:py-[99px] ${sectionBg}`}>
|
||||||
|
<div className="max-w-[1200px] mx-auto px-6 md:px-8">
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
{(heading || subheading) && (
|
||||||
|
<div className="flex flex-col gap-3 mb-10 md:mb-14">
|
||||||
|
{heading && (
|
||||||
|
<h2 className={`font-joey-heavy text-3xl md:text-4xl lg:text-5xl leading-tight ${isNavySection ? 'text-fd-yellow' : 'text-fd-navy'}`}>
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
{subheading && (
|
||||||
|
<p className={`font-joey text-lg md:text-xl ${isNavySection ? 'text-white/70' : 'text-fd-navy/60'}`}>
|
||||||
|
{subheading}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Grid */}
|
||||||
|
<div className={`grid grid-cols-1 ${gridCols} gap-6`}>
|
||||||
|
{members?.map((member, i) => {
|
||||||
|
const photo = member.photo as Media | undefined
|
||||||
|
return (
|
||||||
|
<div key={i} className={`${card.bg} rounded-[70px] overflow-hidden flex flex-col`}>
|
||||||
|
|
||||||
|
{/* Photo */}
|
||||||
|
<div className="aspect-[4/3] w-full overflow-hidden">
|
||||||
|
{photo?.url ? (
|
||||||
|
<FDImage
|
||||||
|
src={photo.url}
|
||||||
|
alt={photo.alt || member.name}
|
||||||
|
width={600}
|
||||||
|
height={450}
|
||||||
|
className="w-full h-full object-cover object-top"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
// Placeholder when no photo
|
||||||
|
<div className={`w-full h-full flex items-center justify-center ${cardStyle === 'navy' ? 'bg-fd-navy/50' : 'bg-fd-navy/10'}`}>
|
||||||
|
<svg viewBox="0 0 80 80" className="w-20 h-20 opacity-30" fill="currentColor">
|
||||||
|
<circle cx="40" cy="30" r="18" />
|
||||||
|
<path d="M10 72c0-16.6 13.4-30 30-30s30 13.4 30 30H10z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className="flex flex-col gap-2 px-8 py-8 flex-1">
|
||||||
|
<p className={`font-joey-bold text-xl ${card.name}`}>{member.name}</p>
|
||||||
|
<p className={`font-joey text-sm ${card.role}`}>{member.role}</p>
|
||||||
|
|
||||||
|
{member.bio && (
|
||||||
|
<p className={`font-joey text-sm leading-relaxed mt-2 ${card.bio}`}>
|
||||||
|
{member.bio}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Links */}
|
||||||
|
{(member.email || member.linkedin) && (
|
||||||
|
<div className="flex items-center gap-4 mt-4">
|
||||||
|
{member.email && (
|
||||||
|
<a
|
||||||
|
href={`mailto:${member.email}`}
|
||||||
|
aria-label={`E-post till ${member.name}`}
|
||||||
|
className={`transition-colors ${card.icon}`}
|
||||||
|
>
|
||||||
|
{/* Email icon */}
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{member.linkedin && (
|
||||||
|
<a
|
||||||
|
href={member.linkedin}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label={`LinkedIn för ${member.name}`}
|
||||||
|
className={`transition-colors ${card.icon}`}
|
||||||
|
>
|
||||||
|
{/* LinkedIn icon */}
|
||||||
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
106
src/blocks/FDTeamBlock/config.ts
Normal file
106
src/blocks/FDTeamBlock/config.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const FDTeamBlock: Block = {
|
||||||
|
slug: 'fdTeam',
|
||||||
|
interfaceName: 'FDTeamBlock',
|
||||||
|
labels: {
|
||||||
|
singular: 'FD Team',
|
||||||
|
plural: 'FD Team',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
label: 'Rubrik (valfri)',
|
||||||
|
admin: {
|
||||||
|
description: 'T.ex. "Möt vårt team"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subheading',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
label: 'Underrubrik (valfri)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'members',
|
||||||
|
type: 'array',
|
||||||
|
label: 'Teammedlemmar',
|
||||||
|
minRows: 1,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'photo',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
label: 'Profilbild',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
label: 'Namn',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'role',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
label: 'Titel / Roll',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bio',
|
||||||
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
|
label: 'Bio (valfri)',
|
||||||
|
admin: {
|
||||||
|
description: 'Kort beskrivning om personen',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
type: 'email',
|
||||||
|
label: 'E-post (valfri)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'linkedin',
|
||||||
|
type: 'text',
|
||||||
|
label: 'LinkedIn URL (valfri)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'columns',
|
||||||
|
type: 'select',
|
||||||
|
label: 'Antal kolumner',
|
||||||
|
defaultValue: '3',
|
||||||
|
options: [
|
||||||
|
{ label: '2 kolumner', value: '2' },
|
||||||
|
{ label: '3 kolumner', value: '3' },
|
||||||
|
{ label: '4 kolumner', value: '4' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cardStyle',
|
||||||
|
type: 'select',
|
||||||
|
label: 'Kortstil',
|
||||||
|
defaultValue: 'navy',
|
||||||
|
options: [
|
||||||
|
{ label: 'Navy', value: 'navy' },
|
||||||
|
{ label: 'Vit', value: 'white' },
|
||||||
|
{ label: 'Grå', value: 'gray' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sectionBackground',
|
||||||
|
type: 'select',
|
||||||
|
label: 'Sektionsbakgrund',
|
||||||
|
defaultValue: 'white',
|
||||||
|
options: [
|
||||||
|
{ label: 'Vit', value: 'white' },
|
||||||
|
{ label: 'Grå', value: 'gray' },
|
||||||
|
{ label: 'Navy', value: 'navy' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
142
src/blocks/FDTestimonialBlock/Component.tsx
Normal file
142
src/blocks/FDTestimonialBlock/Component.tsx
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import type { FDTestimonialBlock as FDTestimonialBlockProps, Media } from '@/payload-types'
|
||||||
|
import { FDImage } from '@/components/FDImage'
|
||||||
|
|
||||||
|
const bgMap: Record<string, { section: string; card: string; quote: string; meta: string; name: string; company: string }> = {
|
||||||
|
gray: { section: 'bg-[#e5e5e5]', card: 'bg-white', quote: 'text-fd-navy', meta: 'text-fd-navy/60', name: 'text-fd-navy', company: 'text-fd-navy/50' },
|
||||||
|
white: { section: 'bg-white', card: 'bg-[#e5e5e5]', quote: 'text-fd-navy', meta: 'text-fd-navy/60', name: 'text-fd-navy', company: 'text-fd-navy/50' },
|
||||||
|
navy: { section: 'bg-fd-navy', card: 'bg-white/10', quote: 'text-white', meta: 'text-white/60', name: 'text-white', company: 'text-white/50' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FDTestimonialBlockComponent: React.FC<FDTestimonialBlockProps> = ({
|
||||||
|
heading,
|
||||||
|
testimonials,
|
||||||
|
layout = 'grid',
|
||||||
|
sectionBackground = 'gray',
|
||||||
|
}) => {
|
||||||
|
const theme = bgMap[sectionBackground] || bgMap.gray
|
||||||
|
const isFeatured = layout === 'featured'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={`w-full py-16 md:py-20 lg:py-[99px] ${theme.section}`}>
|
||||||
|
<div className="max-w-[1200px] mx-auto px-6 md:px-8">
|
||||||
|
|
||||||
|
{heading && (
|
||||||
|
<h2 className={`font-joey-heavy text-3xl md:text-4xl lg:text-5xl leading-tight mb-10 md:mb-14 ${sectionBackground === 'navy' ? 'text-fd-yellow' : 'text-fd-navy'}`}>
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isFeatured && testimonials && testimonials.length > 0 ? (
|
||||||
|
// ── Featured layout: first testimonial large, rest below ──────
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
{/* First testimonial — large */}
|
||||||
|
{(() => {
|
||||||
|
const t = testimonials[0]
|
||||||
|
const avatar = t.avatar as Media | undefined
|
||||||
|
return (
|
||||||
|
<div className={`${theme.card} rounded-[70px] px-10 md:px-16 py-12 md:py-16 flex flex-col gap-8`}>
|
||||||
|
<p className={`font-joey-medium text-xl md:text-2xl lg:text-3xl leading-relaxed ${theme.quote}`}>
|
||||||
|
“{t.quote}”
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{avatar?.url && (
|
||||||
|
<div className="w-14 h-14 rounded-full overflow-hidden flex-shrink-0">
|
||||||
|
<FDImage
|
||||||
|
src={avatar.url}
|
||||||
|
alt={avatar.alt || t.authorName}
|
||||||
|
width={56}
|
||||||
|
height={56}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className={`font-joey-bold text-lg ${theme.name}`}>{t.authorName}</p>
|
||||||
|
<p className={`font-joey text-sm ${theme.meta}`}>
|
||||||
|
{t.authorRole}{t.authorRole && t.authorCompany ? ' · ' : ''}{t.authorCompany}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{/* Remaining testimonials — smaller grid */}
|
||||||
|
{testimonials.length > 1 && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{testimonials.slice(1).map((t, i) => {
|
||||||
|
const avatar = t.avatar as Media | undefined
|
||||||
|
return (
|
||||||
|
<div key={i} className={`${theme.card} rounded-[70px] px-8 md:px-12 py-10 md:py-12 flex flex-col gap-6`}>
|
||||||
|
<p className={`font-joey-medium text-lg md:text-xl leading-relaxed ${theme.quote}`}>
|
||||||
|
“{t.quote}”
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{avatar?.url && (
|
||||||
|
<div className="w-10 h-10 rounded-full overflow-hidden flex-shrink-0">
|
||||||
|
<FDImage
|
||||||
|
src={avatar.url}
|
||||||
|
alt={avatar.alt || t.authorName}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className={`font-joey-bold text-base ${theme.name}`}>{t.authorName}</p>
|
||||||
|
<p className={`font-joey text-xs ${theme.meta}`}>
|
||||||
|
{t.authorRole}{t.authorRole && t.authorCompany ? ' · ' : ''}{t.authorCompany}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// ── Grid layout ───────────────────────────────────────────────
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{testimonials?.map((t, i) => {
|
||||||
|
const avatar = t.avatar as Media | undefined
|
||||||
|
return (
|
||||||
|
<div key={i} className={`${theme.card} rounded-[70px] px-8 md:px-10 py-10 md:py-12 flex flex-col gap-6`}>
|
||||||
|
{/* Quote mark */}
|
||||||
|
<span className={`font-joey-heavy text-5xl leading-none ${sectionBackground === 'navy' ? 'text-fd-yellow' : 'text-fd-navy'} opacity-30`}>
|
||||||
|
“
|
||||||
|
</span>
|
||||||
|
<p className={`font-joey-medium text-lg leading-relaxed -mt-4 ${theme.quote}`}>
|
||||||
|
{t.quote}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-3 mt-auto">
|
||||||
|
{avatar?.url && (
|
||||||
|
<div className="w-10 h-10 rounded-full overflow-hidden flex-shrink-0">
|
||||||
|
<FDImage
|
||||||
|
src={avatar.url}
|
||||||
|
alt={avatar.alt || t.authorName}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className={`font-joey-bold text-base ${theme.name}`}>{t.authorName}</p>
|
||||||
|
<p className={`font-joey text-sm ${theme.meta}`}>
|
||||||
|
{t.authorRole}{t.authorRole && t.authorCompany ? ' · ' : ''}{t.authorCompany}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
87
src/blocks/FDTestimonialBlock/config.ts
Normal file
87
src/blocks/FDTestimonialBlock/config.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const FDTestimonialBlock: Block = {
|
||||||
|
slug: 'fdTestimonial',
|
||||||
|
interfaceName: 'FDTestimonialBlock',
|
||||||
|
labels: {
|
||||||
|
singular: 'FD Omdöme',
|
||||||
|
plural: 'FD Omdömen',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'heading',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
label: 'Rubrik (valfri)',
|
||||||
|
admin: {
|
||||||
|
description: 'T.ex. "Vad våra kunder säger"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'testimonials',
|
||||||
|
type: 'array',
|
||||||
|
label: 'Omdömen',
|
||||||
|
minRows: 1,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'quote',
|
||||||
|
type: 'textarea',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
label: 'Citat',
|
||||||
|
admin: {
|
||||||
|
description: 'Kundens citat utan citattecken',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'authorName',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
localized: true,
|
||||||
|
label: 'Namn',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'authorRole',
|
||||||
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
|
label: 'Titel / Roll',
|
||||||
|
admin: {
|
||||||
|
description: 'T.ex. "IT-chef"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'authorCompany',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Företag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'avatar',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
label: 'Profilbild (valfri)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'layout',
|
||||||
|
type: 'select',
|
||||||
|
label: 'Layout',
|
||||||
|
defaultValue: 'grid',
|
||||||
|
options: [
|
||||||
|
{ label: 'Rutnät', value: 'grid' },
|
||||||
|
{ label: 'Stor featured', value: 'featured' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sectionBackground',
|
||||||
|
type: 'select',
|
||||||
|
label: 'Sektionsbakgrund',
|
||||||
|
defaultValue: 'gray',
|
||||||
|
options: [
|
||||||
|
{ label: 'Grå', value: 'gray' },
|
||||||
|
{ label: 'Vit', value: 'white' },
|
||||||
|
{ label: 'Navy', value: 'navy' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@ -35,6 +35,9 @@ import { FDDataTableBlockComponent } from '@/blocks/FDDataTableBlock/Component'
|
|||||||
import { FDVpsCalculatorBlockComponent } from '@/blocks/FDVpsCalculatorBlock/Component'
|
import { FDVpsCalculatorBlockComponent } from '@/blocks/FDVpsCalculatorBlock/Component'
|
||||||
import { FDCodeEmbedBlockComponent } from '@/blocks/FDCodeEmbedBlock/Component'
|
import { FDCodeEmbedBlockComponent } from '@/blocks/FDCodeEmbedBlock/Component'
|
||||||
import { FDVideoBlockComponent } from '@/blocks/FDVideoBlock/Component'
|
import { FDVideoBlockComponent } from '@/blocks/FDVideoBlock/Component'
|
||||||
|
import { FDCtaBannerBlockComponent } from './FDCtaBannerBlock/Component'
|
||||||
|
import { FDTestimonialBlockComponent } from './FDTestimonialBlock/Component'
|
||||||
|
import { FDTeamBlockComponent } from './FDTeamBlock/Component'
|
||||||
|
|
||||||
const blockComponents = {
|
const blockComponents = {
|
||||||
archive: ArchiveBlock,
|
archive: ArchiveBlock,
|
||||||
@ -70,6 +73,9 @@ const blockComponents = {
|
|||||||
fdVpsCalculator: FDVpsCalculatorBlockComponent,
|
fdVpsCalculator: FDVpsCalculatorBlockComponent,
|
||||||
fdCodeEmbed: FDCodeEmbedBlockComponent,
|
fdCodeEmbed: FDCodeEmbedBlockComponent,
|
||||||
fdVideo: FDVideoBlockComponent,
|
fdVideo: FDVideoBlockComponent,
|
||||||
|
fdCtaBanner: FDCtaBannerBlockComponent,
|
||||||
|
fdTestimonial: FDTestimonialBlockComponent,
|
||||||
|
fdTeam: FDTeamBlockComponent,
|
||||||
}
|
}
|
||||||
|
|
||||||
// FD blocks handle their own full-width sections with internal padding
|
// FD blocks handle their own full-width sections with internal padding
|
||||||
@ -102,6 +108,9 @@ const fullWidthBlocks = new Set([
|
|||||||
'fdVpsCalculator',
|
'fdVpsCalculator',
|
||||||
'fdCodeEmbed',
|
'fdCodeEmbed',
|
||||||
'fdVideo',
|
'fdVideo',
|
||||||
|
'fdCtaBanner',
|
||||||
|
'fdTestimonial',
|
||||||
|
'fdTeam',
|
||||||
])
|
])
|
||||||
|
|
||||||
export const RenderBlocks: React.FC<{
|
export const RenderBlocks: React.FC<{
|
||||||
|
|||||||
@ -34,6 +34,9 @@ import { FDDataTableBlock } from '../../blocks/FDDataTableBlock/config'
|
|||||||
import { FDVpsCalculatorBlock } from '../../blocks/FDVpsCalculatorBlock/config'
|
import { FDVpsCalculatorBlock } from '../../blocks/FDVpsCalculatorBlock/config'
|
||||||
import { FDCodeEmbedBlock } from '../../blocks/FDCodeEmbedBlock/config'
|
import { FDCodeEmbedBlock } from '../../blocks/FDCodeEmbedBlock/config'
|
||||||
import { FDVideoBlock } from '../../blocks/FDVideoBlock/config'
|
import { FDVideoBlock } from '../../blocks/FDVideoBlock/config'
|
||||||
|
import { FDCtaBannerBlock } from '../../blocks/FDCtaBannerBlock/config'
|
||||||
|
import { FDTestimonialBlock } from '../../blocks/FDTestimonialBlock/config'
|
||||||
|
import { FDTeamBlock } from '../../blocks/FDTeamBlock/config'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MetaDescriptionField,
|
MetaDescriptionField,
|
||||||
@ -126,6 +129,9 @@ export const Pages: CollectionConfig<'pages'> = {
|
|||||||
FDTextBlock,
|
FDTextBlock,
|
||||||
FDCodeEmbedBlock,
|
FDCodeEmbedBlock,
|
||||||
FDVideoBlock,
|
FDVideoBlock,
|
||||||
|
FDCtaBannerBlock,
|
||||||
|
FDTestimonialBlock,
|
||||||
|
FDTeamBlock
|
||||||
],
|
],
|
||||||
required: true,
|
required: true,
|
||||||
admin: {
|
admin: {
|
||||||
|
|||||||
@ -182,6 +182,9 @@ export interface Page {
|
|||||||
| FDTextBlock
|
| FDTextBlock
|
||||||
| FDCodeEmbedBlock
|
| FDCodeEmbedBlock
|
||||||
| FDVideoBlock
|
| FDVideoBlock
|
||||||
|
| FDCtaBannerBlock
|
||||||
|
| FDTestimonialBlock
|
||||||
|
| FDTeamBlock
|
||||||
)[];
|
)[];
|
||||||
meta?: {
|
meta?: {
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
@ -1236,6 +1239,89 @@ export interface FDVideoBlock {
|
|||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'fdVideo';
|
blockType: 'fdVideo';
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "FDCtaBannerBlock".
|
||||||
|
*/
|
||||||
|
export interface FDCtaBannerBlock {
|
||||||
|
heading: string;
|
||||||
|
/**
|
||||||
|
* Kort text under rubriken
|
||||||
|
*/
|
||||||
|
subheading?: string | null;
|
||||||
|
ctaText?: string | null;
|
||||||
|
ctaLink?: string | null;
|
||||||
|
secondaryCtaText?: string | null;
|
||||||
|
secondaryCtaLink?: string | null;
|
||||||
|
sectionBackground?: ('yellow' | 'navy' | 'gray' | 'white') | null;
|
||||||
|
alignment?: ('center' | 'left') | null;
|
||||||
|
size?: ('small' | 'medium' | 'large') | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'fdCtaBanner';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "FDTestimonialBlock".
|
||||||
|
*/
|
||||||
|
export interface FDTestimonialBlock {
|
||||||
|
/**
|
||||||
|
* T.ex. "Vad våra kunder säger"
|
||||||
|
*/
|
||||||
|
heading?: string | null;
|
||||||
|
testimonials?:
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* Kundens citat utan citattecken
|
||||||
|
*/
|
||||||
|
quote: string;
|
||||||
|
authorName: string;
|
||||||
|
/**
|
||||||
|
* T.ex. "IT-chef"
|
||||||
|
*/
|
||||||
|
authorRole?: string | null;
|
||||||
|
authorCompany?: string | null;
|
||||||
|
avatar?: (number | null) | Media;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
layout?: ('grid' | 'featured') | null;
|
||||||
|
sectionBackground?: ('gray' | 'white' | 'navy') | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'fdTestimonial';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "FDTeamBlock".
|
||||||
|
*/
|
||||||
|
export interface FDTeamBlock {
|
||||||
|
/**
|
||||||
|
* T.ex. "Möt vårt team"
|
||||||
|
*/
|
||||||
|
heading?: string | null;
|
||||||
|
subheading?: string | null;
|
||||||
|
members?:
|
||||||
|
| {
|
||||||
|
photo?: (number | null) | Media;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
/**
|
||||||
|
* Kort beskrivning om personen
|
||||||
|
*/
|
||||||
|
bio?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
linkedin?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
columns?: ('2' | '3' | '4') | null;
|
||||||
|
cardStyle?: ('navy' | 'white' | 'gray') | null;
|
||||||
|
sectionBackground?: ('white' | 'gray' | 'navy') | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'fdTeam';
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "posts".
|
* via the `definition` "posts".
|
||||||
@ -1601,6 +1687,9 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
fdText?: T | FDTextBlockSelect<T>;
|
fdText?: T | FDTextBlockSelect<T>;
|
||||||
fdCodeEmbed?: T | FDCodeEmbedBlockSelect<T>;
|
fdCodeEmbed?: T | FDCodeEmbedBlockSelect<T>;
|
||||||
fdVideo?: T | FDVideoBlockSelect<T>;
|
fdVideo?: T | FDVideoBlockSelect<T>;
|
||||||
|
fdCtaBanner?: T | FDCtaBannerBlockSelect<T>;
|
||||||
|
fdTestimonial?: T | FDTestimonialBlockSelect<T>;
|
||||||
|
fdTeam?: T | FDTeamBlockSelect<T>;
|
||||||
};
|
};
|
||||||
meta?:
|
meta?:
|
||||||
| T
|
| T
|
||||||
@ -2180,6 +2269,68 @@ export interface FDVideoBlockSelect<T extends boolean = true> {
|
|||||||
id?: T;
|
id?: T;
|
||||||
blockName?: T;
|
blockName?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "FDCtaBannerBlock_select".
|
||||||
|
*/
|
||||||
|
export interface FDCtaBannerBlockSelect<T extends boolean = true> {
|
||||||
|
heading?: T;
|
||||||
|
subheading?: T;
|
||||||
|
ctaText?: T;
|
||||||
|
ctaLink?: T;
|
||||||
|
secondaryCtaText?: T;
|
||||||
|
secondaryCtaLink?: T;
|
||||||
|
sectionBackground?: T;
|
||||||
|
alignment?: T;
|
||||||
|
size?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "FDTestimonialBlock_select".
|
||||||
|
*/
|
||||||
|
export interface FDTestimonialBlockSelect<T extends boolean = true> {
|
||||||
|
heading?: T;
|
||||||
|
testimonials?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
quote?: T;
|
||||||
|
authorName?: T;
|
||||||
|
authorRole?: T;
|
||||||
|
authorCompany?: T;
|
||||||
|
avatar?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
layout?: T;
|
||||||
|
sectionBackground?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "FDTeamBlock_select".
|
||||||
|
*/
|
||||||
|
export interface FDTeamBlockSelect<T extends boolean = true> {
|
||||||
|
heading?: T;
|
||||||
|
subheading?: T;
|
||||||
|
members?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
photo?: T;
|
||||||
|
name?: T;
|
||||||
|
role?: T;
|
||||||
|
bio?: T;
|
||||||
|
email?: T;
|
||||||
|
linkedin?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
columns?: T;
|
||||||
|
cardStyle?: T;
|
||||||
|
sectionBackground?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "posts_select".
|
* via the `definition` "posts_select".
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user