- Add ImagePickerField to all site builder components with image URLs:
- Marketing: Hero, SplitContent, LogoCloud, GalleryGrid, Testimonials,
ContentBlocks, Header, Footer
- Email: EmailImage, EmailHeader
- Fix media gallery issues:
- Change API endpoint from /media/ to /media-files/ to avoid URL conflict
- Fix album file_count annotation conflict with model property
- Fix image URLs to use absolute paths for cross-domain access
- Add clipboard fallback for non-HTTPS copy URL
- Show permission slugs in plan feature editor
- Fix branding settings access for tenants with custom_branding feature
- Fix EntitlementService method call in StorageQuotaService
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
268 lines
7.8 KiB
TypeScript
268 lines
7.8 KiB
TypeScript
import React from 'react';
|
|
import type { ComponentConfig } from '@measured/puck';
|
|
import {
|
|
createDesignControlsFields,
|
|
applyDesignControls,
|
|
type DesignControlsProps,
|
|
} from '../../theme';
|
|
import { imagePickerField } from '../../fields/ImagePickerField';
|
|
|
|
export interface HeroCtaButton {
|
|
text: string;
|
|
href: string;
|
|
}
|
|
|
|
export interface HeroMedia {
|
|
type: 'image' | 'none';
|
|
src?: string;
|
|
alt?: string;
|
|
}
|
|
|
|
export interface HeroProps extends DesignControlsProps {
|
|
headline: string;
|
|
subheadline: string;
|
|
primaryCta: HeroCtaButton;
|
|
secondaryCta?: HeroCtaButton;
|
|
media?: HeroMedia;
|
|
badge?: string;
|
|
variant: 'centered' | 'split' | 'minimal';
|
|
fullWidth?: boolean;
|
|
}
|
|
|
|
const HeroRender: React.FC<HeroProps> = (props) => {
|
|
const {
|
|
headline,
|
|
subheadline,
|
|
primaryCta,
|
|
secondaryCta,
|
|
media,
|
|
badge,
|
|
variant,
|
|
fullWidth,
|
|
...designControls
|
|
} = props;
|
|
|
|
const applied = applyDesignControls(designControls);
|
|
const isGradient = designControls.backgroundVariant === 'gradient';
|
|
const isDark = isGradient || designControls.backgroundVariant === 'dark';
|
|
|
|
const isSplit = variant === 'split';
|
|
const isMinimal = variant === 'minimal';
|
|
|
|
const containerClass = fullWidth
|
|
? 'w-full px-4 sm:px-6 lg:px-8'
|
|
: `${applied.containerClassName || 'max-w-7xl'} mx-auto px-4 sm:px-6 lg:px-8`;
|
|
|
|
return (
|
|
<section
|
|
id={designControls.anchorId || undefined}
|
|
className={`relative overflow-hidden ${applied.className}`}
|
|
style={applied.style}
|
|
>
|
|
{/* Overlay for background images */}
|
|
{designControls.backgroundImage && designControls.overlayStrength && (
|
|
<div
|
|
className="absolute inset-0 bg-black pointer-events-none"
|
|
style={{ opacity: designControls.overlayStrength / 100 }}
|
|
/>
|
|
)}
|
|
|
|
<div className={`relative ${containerClass} py-16 sm:py-24 lg:py-32`}>
|
|
<div
|
|
className={
|
|
isSplit
|
|
? 'grid grid-cols-1 lg:grid-cols-2 gap-12 items-center'
|
|
: 'text-center max-w-4xl mx-auto'
|
|
}
|
|
>
|
|
<div>
|
|
{/* Badge */}
|
|
{badge && !isMinimal && (
|
|
<div className={`mb-6 ${isSplit ? '' : ''}`}>
|
|
<span
|
|
className={`inline-block px-4 py-1.5 rounded-full text-sm font-medium ${
|
|
isDark
|
|
? 'bg-white/10 text-white border border-white/20'
|
|
: 'bg-primary-100 text-primary-700'
|
|
}`}
|
|
>
|
|
{badge}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Headline */}
|
|
<h1
|
|
className={`text-4xl sm:text-5xl ${isMinimal ? '' : 'lg:text-6xl'} font-bold tracking-tight ${
|
|
isDark ? 'text-white' : 'text-neutral-900'
|
|
}`}
|
|
>
|
|
{headline}
|
|
</h1>
|
|
|
|
{/* Subheadline */}
|
|
<p
|
|
className={`mt-6 text-lg sm:text-xl ${isSplit ? '' : 'max-w-2xl mx-auto'} ${
|
|
isDark ? 'text-white/80' : 'text-neutral-600'
|
|
}`}
|
|
>
|
|
{subheadline}
|
|
</p>
|
|
|
|
{/* CTAs */}
|
|
{(primaryCta?.text || secondaryCta?.text) && (
|
|
<div
|
|
className={`mt-10 flex flex-col sm:flex-row gap-4 ${
|
|
isSplit ? '' : 'items-center justify-center'
|
|
}`}
|
|
>
|
|
{primaryCta?.text && (
|
|
<a
|
|
href={primaryCta.href || '#'}
|
|
className={`w-full sm:w-auto px-8 py-3 rounded-lg font-semibold text-center transition-colors ${
|
|
isDark
|
|
? 'bg-white text-neutral-900 hover:bg-neutral-100'
|
|
: 'bg-primary-600 text-white hover:bg-primary-700'
|
|
}`}
|
|
>
|
|
{primaryCta.text}
|
|
</a>
|
|
)}
|
|
{secondaryCta?.text && (
|
|
<a
|
|
href={secondaryCta.href || '#'}
|
|
className={`w-full sm:w-auto px-8 py-3 rounded-lg font-semibold text-center transition-colors ${
|
|
isDark
|
|
? 'bg-transparent text-white border border-white/30 hover:bg-white/10'
|
|
: 'bg-transparent text-neutral-700 border border-neutral-300 hover:bg-neutral-50'
|
|
}`}
|
|
>
|
|
{secondaryCta.text}
|
|
</a>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Media */}
|
|
{media?.type === 'image' && media.src && (
|
|
<div className={isSplit ? '' : 'mt-16'}>
|
|
<div className={`relative ${isSplit ? '' : 'mx-auto max-w-5xl'}`}>
|
|
<div
|
|
className={`rounded-xl overflow-hidden shadow-2xl ${
|
|
isDark ? 'ring-1 ring-white/10' : 'ring-1 ring-neutral-200'
|
|
}`}
|
|
>
|
|
<img
|
|
src={media.src}
|
|
alt={media.alt || 'Hero image'}
|
|
className="w-full h-auto"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const designControlFields = createDesignControlsFields();
|
|
|
|
export const Hero: ComponentConfig<HeroProps> = {
|
|
label: 'Hero',
|
|
fields: {
|
|
headline: {
|
|
type: 'text',
|
|
label: 'Headline',
|
|
},
|
|
subheadline: {
|
|
type: 'textarea',
|
|
label: 'Subheadline',
|
|
},
|
|
primaryCta: {
|
|
type: 'object',
|
|
label: 'Primary Button',
|
|
objectFields: {
|
|
text: { type: 'text', label: 'Button Text' },
|
|
href: { type: 'text', label: 'Button URL' },
|
|
},
|
|
},
|
|
secondaryCta: {
|
|
type: 'object',
|
|
label: 'Secondary Button (optional)',
|
|
objectFields: {
|
|
text: { type: 'text', label: 'Button Text' },
|
|
href: { type: 'text', label: 'Button URL' },
|
|
},
|
|
},
|
|
media: {
|
|
type: 'object',
|
|
label: 'Media',
|
|
objectFields: {
|
|
type: {
|
|
type: 'select',
|
|
label: 'Type',
|
|
options: [
|
|
{ label: 'None', value: 'none' },
|
|
{ label: 'Image', value: 'image' },
|
|
],
|
|
},
|
|
src: { ...imagePickerField, label: 'Image' },
|
|
alt: { type: 'text', label: 'Alt Text' },
|
|
},
|
|
},
|
|
badge: {
|
|
type: 'text',
|
|
label: 'Badge Text (optional)',
|
|
},
|
|
variant: {
|
|
type: 'select',
|
|
label: 'Layout Variant',
|
|
options: [
|
|
{ label: 'Centered', value: 'centered' },
|
|
{ label: 'Split (text + media)', value: 'split' },
|
|
{ label: 'Minimal', value: 'minimal' },
|
|
],
|
|
},
|
|
fullWidth: {
|
|
type: 'radio',
|
|
label: 'Full Width',
|
|
options: [
|
|
{ label: 'No', value: false },
|
|
{ label: 'Yes', value: true },
|
|
],
|
|
},
|
|
// Design Controls
|
|
padding: designControlFields.padding,
|
|
backgroundVariant: designControlFields.backgroundVariant,
|
|
gradientPreset: designControlFields.gradientPreset,
|
|
backgroundImage: designControlFields.backgroundImage,
|
|
overlayStrength: designControlFields.overlayStrength,
|
|
contentMaxWidth: designControlFields.contentMaxWidth,
|
|
alignment: designControlFields.alignment,
|
|
hideOnMobile: designControlFields.hideOnMobile,
|
|
hideOnTablet: designControlFields.hideOnTablet,
|
|
hideOnDesktop: designControlFields.hideOnDesktop,
|
|
anchorId: designControlFields.anchorId,
|
|
},
|
|
defaultProps: {
|
|
headline: 'Your headline goes here',
|
|
subheadline: 'Add a compelling subheadline that describes what you offer.',
|
|
primaryCta: {
|
|
text: 'Get Started',
|
|
href: '#',
|
|
},
|
|
variant: 'centered',
|
|
fullWidth: false,
|
|
backgroundVariant: 'none',
|
|
padding: 'xl',
|
|
contentMaxWidth: 'wide',
|
|
alignment: 'center',
|
|
},
|
|
render: HeroRender,
|
|
};
|
|
|
|
export default Hero;
|