diff --git a/media/Screenshot 2026-02-18 at 13.41.09.png b/media/Screenshot 2026-02-18 at 13.41.09.png new file mode 100644 index 0000000..2a4905b Binary files /dev/null and b/media/Screenshot 2026-02-18 at 13.41.09.png differ diff --git a/src/blocks/FDCtaBannerBlock/Component.tsx b/src/blocks/FDCtaBannerBlock/Component.tsx new file mode 100644 index 0000000..7e7f9f8 --- /dev/null +++ b/src/blocks/FDCtaBannerBlock/Component.tsx @@ -0,0 +1,97 @@ +import React from 'react' +import type { FDCtaBannerBlock as FDCtaBannerBlockProps } from '@/payload-types' + +const bgMap: Record = { + 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 = { + 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 = ({ + 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 ( +
+
+
+ +
+

+ {heading} +

+ {subheading && ( +

+ {subheading} +

+ )} +
+ + {(ctaText || secondaryCtaText) && ( +
+ {ctaText && ( + + {ctaText} + + )} + {secondaryCtaText && secondaryCtaLink && ( + + {secondaryCtaText} + + )} +
+ )} + +
+
+
+ ) +} diff --git a/src/blocks/FDCtaBannerBlock/config.ts b/src/blocks/FDCtaBannerBlock/config.ts new file mode 100644 index 0000000..cae62c9 --- /dev/null +++ b/src/blocks/FDCtaBannerBlock/config.ts @@ -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' }, + ], + }, + ], +} diff --git a/src/blocks/FDTeamBlock/Component.tsx b/src/blocks/FDTeamBlock/Component.tsx new file mode 100644 index 0000000..4a92891 --- /dev/null +++ b/src/blocks/FDTeamBlock/Component.tsx @@ -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 = { + white: 'bg-white', + gray: 'bg-[#e5e5e5]', + navy: 'bg-fd-navy', +} + +const cardMap: Record = { + 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 = { + '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 = ({ + 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 ( +
+
+ + {/* Header */} + {(heading || subheading) && ( +
+ {heading && ( +

+ {heading} +

+ )} + {subheading && ( +

+ {subheading} +

+ )} +
+ )} + + {/* Grid */} +
+ {members?.map((member, i) => { + const photo = member.photo as Media | undefined + return ( +
+ + {/* Photo */} +
+ {photo?.url ? ( + + ) : ( + // Placeholder when no photo +
+ + + + +
+ )} +
+ + {/* Info */} +
+

{member.name}

+

{member.role}

+ + {member.bio && ( +

+ {member.bio} +

+ )} + + {/* Links */} + {(member.email || member.linkedin) && ( +
+ {member.email && ( + + {/* Email icon */} + + + + + )} + {member.linkedin && ( + + {/* LinkedIn icon */} + + + + + )} +
+ )} +
+ +
+ ) + })} +
+ +
+
+ ) +} diff --git a/src/blocks/FDTeamBlock/config.ts b/src/blocks/FDTeamBlock/config.ts new file mode 100644 index 0000000..d978a16 --- /dev/null +++ b/src/blocks/FDTeamBlock/config.ts @@ -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' }, + ], + }, + ], +} diff --git a/src/blocks/FDTestimonialBlock/Component.tsx b/src/blocks/FDTestimonialBlock/Component.tsx new file mode 100644 index 0000000..08177e7 --- /dev/null +++ b/src/blocks/FDTestimonialBlock/Component.tsx @@ -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 = { + 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 = ({ + heading, + testimonials, + layout = 'grid', + sectionBackground = 'gray', +}) => { + const theme = bgMap[sectionBackground] || bgMap.gray + const isFeatured = layout === 'featured' + + return ( +
+
+ + {heading && ( +

+ {heading} +

+ )} + + {isFeatured && testimonials && testimonials.length > 0 ? ( + // ── Featured layout: first testimonial large, rest below ────── +
+ {/* First testimonial — large */} + {(() => { + const t = testimonials[0] + const avatar = t.avatar as Media | undefined + return ( +
+

+ “{t.quote}” +

+
+ {avatar?.url && ( +
+ +
+ )} +
+

{t.authorName}

+

+ {t.authorRole}{t.authorRole && t.authorCompany ? ' · ' : ''}{t.authorCompany} +

+
+
+
+ ) + })()} + + {/* Remaining testimonials — smaller grid */} + {testimonials.length > 1 && ( +
+ {testimonials.slice(1).map((t, i) => { + const avatar = t.avatar as Media | undefined + return ( +
+

+ “{t.quote}” +

+
+ {avatar?.url && ( +
+ +
+ )} +
+

{t.authorName}

+

+ {t.authorRole}{t.authorRole && t.authorCompany ? ' · ' : ''}{t.authorCompany} +

+
+
+
+ ) + })} +
+ )} +
+ ) : ( + // ── Grid layout ─────────────────────────────────────────────── +
+ {testimonials?.map((t, i) => { + const avatar = t.avatar as Media | undefined + return ( +
+ {/* Quote mark */} + + “ + +

+ {t.quote} +

+
+ {avatar?.url && ( +
+ +
+ )} +
+

{t.authorName}

+

+ {t.authorRole}{t.authorRole && t.authorCompany ? ' · ' : ''}{t.authorCompany} +

+
+
+
+ ) + })} +
+ )} + +
+
+ ) +} diff --git a/src/blocks/FDTestimonialBlock/config.ts b/src/blocks/FDTestimonialBlock/config.ts new file mode 100644 index 0000000..d6acea9 --- /dev/null +++ b/src/blocks/FDTestimonialBlock/config.ts @@ -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' }, + ], + }, + ], +} diff --git a/src/blocks/RenderBlocks.tsx b/src/blocks/RenderBlocks.tsx index f52d1ef..fae657d 100644 --- a/src/blocks/RenderBlocks.tsx +++ b/src/blocks/RenderBlocks.tsx @@ -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<{ diff --git a/src/collections/Pages/index.ts b/src/collections/Pages/index.ts index 235d057..f881586 100644 --- a/src/collections/Pages/index.ts +++ b/src/collections/Pages/index.ts @@ -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: { diff --git a/src/payload-types.ts b/src/payload-types.ts index 5ea4e11..a0a1af9 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -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 { fdText?: T | FDTextBlockSelect; fdCodeEmbed?: T | FDCodeEmbedBlockSelect; fdVideo?: T | FDVideoBlockSelect; + fdCtaBanner?: T | FDCtaBannerBlockSelect; + fdTestimonial?: T | FDTestimonialBlockSelect; + fdTeam?: T | FDTeamBlockSelect; }; meta?: | T @@ -2180,6 +2269,68 @@ export interface FDVideoBlockSelect { id?: T; blockName?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "FDCtaBannerBlock_select". + */ +export interface FDCtaBannerBlockSelect { + 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 { + 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 { + 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".