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 { FDCodeEmbedBlockComponent } from '@/blocks/FDCodeEmbedBlock/Component'
|
||||
import { FDVideoBlockComponent } from '@/blocks/FDVideoBlock/Component'
|
||||
import { FDCtaBannerBlockComponent } from './FDCtaBannerBlock/Component'
|
||||
import { FDTestimonialBlockComponent } from './FDTestimonialBlock/Component'
|
||||
import { FDTeamBlockComponent } from './FDTeamBlock/Component'
|
||||
|
||||
const blockComponents = {
|
||||
archive: ArchiveBlock,
|
||||
@ -70,6 +73,9 @@ const blockComponents = {
|
||||
fdVpsCalculator: FDVpsCalculatorBlockComponent,
|
||||
fdCodeEmbed: FDCodeEmbedBlockComponent,
|
||||
fdVideo: FDVideoBlockComponent,
|
||||
fdCtaBanner: FDCtaBannerBlockComponent,
|
||||
fdTestimonial: FDTestimonialBlockComponent,
|
||||
fdTeam: FDTeamBlockComponent,
|
||||
}
|
||||
|
||||
// FD blocks handle their own full-width sections with internal padding
|
||||
@ -102,6 +108,9 @@ const fullWidthBlocks = new Set([
|
||||
'fdVpsCalculator',
|
||||
'fdCodeEmbed',
|
||||
'fdVideo',
|
||||
'fdCtaBanner',
|
||||
'fdTestimonial',
|
||||
'fdTeam',
|
||||
])
|
||||
|
||||
export const RenderBlocks: React.FC<{
|
||||
|
||||
@ -34,6 +34,9 @@ import { FDDataTableBlock } from '../../blocks/FDDataTableBlock/config'
|
||||
import { FDVpsCalculatorBlock } from '../../blocks/FDVpsCalculatorBlock/config'
|
||||
import { FDCodeEmbedBlock } from '../../blocks/FDCodeEmbedBlock/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 {
|
||||
MetaDescriptionField,
|
||||
@ -126,6 +129,9 @@ export const Pages: CollectionConfig<'pages'> = {
|
||||
FDTextBlock,
|
||||
FDCodeEmbedBlock,
|
||||
FDVideoBlock,
|
||||
FDCtaBannerBlock,
|
||||
FDTestimonialBlock,
|
||||
FDTeamBlock
|
||||
],
|
||||
required: true,
|
||||
admin: {
|
||||
|
||||
@ -182,6 +182,9 @@ export interface Page {
|
||||
| FDTextBlock
|
||||
| FDCodeEmbedBlock
|
||||
| FDVideoBlock
|
||||
| FDCtaBannerBlock
|
||||
| FDTestimonialBlock
|
||||
| FDTeamBlock
|
||||
)[];
|
||||
meta?: {
|
||||
title?: string | null;
|
||||
@ -1236,6 +1239,89 @@ export interface FDVideoBlock {
|
||||
blockName?: string | null;
|
||||
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
|
||||
* via the `definition` "posts".
|
||||
@ -1601,6 +1687,9 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
fdText?: T | FDTextBlockSelect<T>;
|
||||
fdCodeEmbed?: T | FDCodeEmbedBlockSelect<T>;
|
||||
fdVideo?: T | FDVideoBlockSelect<T>;
|
||||
fdCtaBanner?: T | FDCtaBannerBlockSelect<T>;
|
||||
fdTestimonial?: T | FDTestimonialBlockSelect<T>;
|
||||
fdTeam?: T | FDTeamBlockSelect<T>;
|
||||
};
|
||||
meta?:
|
||||
| T
|
||||
@ -2180,6 +2269,68 @@ export interface FDVideoBlockSelect<T extends boolean = true> {
|
||||
id?: 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
|
||||
* via the `definition` "posts_select".
|
||||
|
||||
Loading…
Reference in New Issue
Block a user