Files
smoothschedule/frontend/src/puck/components/marketing/Footer.tsx
poduck fbefccf436 Add media gallery with album organization and Puck integration
Backend:
- Add Album and MediaFile models for tenant-scoped media storage
- Add TenantStorageUsage model for per-tenant storage quota tracking
- Create StorageQuotaService with EntitlementService integration
- Add AlbumViewSet, MediaFileViewSet with bulk operations
- Add StorageUsageView for quota monitoring

Frontend:
- Create MediaGalleryPage with album management and file upload
- Add drag-and-drop upload with storage quota validation
- Create ImagePickerField custom Puck field for gallery integration
- Update Image, Testimonial components to use ImagePicker
- Add background image picker to Puck design controls
- Add gallery to sidebar navigation

Also includes:
- Puck marketing components (Hero, SplitContent, etc.)
- Enhanced ContactForm and BusinessHours components
- Platform login page improvements
- Site builder draft/preview enhancements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 19:59:31 -05:00

249 lines
6.9 KiB
TypeScript

import React from 'react';
import type { ComponentConfig } from '@measured/puck';
import { Twitter, Linkedin, Github, Facebook, Instagram, Youtube } from 'lucide-react';
export interface FooterLink {
label: string;
href: string;
}
export interface FooterColumn {
title: string;
links: FooterLink[];
}
export interface SocialLinks {
twitter?: string;
linkedin?: string;
github?: string;
facebook?: string;
instagram?: string;
youtube?: string;
}
export interface MiniCta {
text: string;
placeholder: string;
buttonText: string;
}
export interface FooterProps {
brandText: string;
brandLogo?: string;
description?: string;
columns: FooterColumn[];
socialLinks?: SocialLinks;
smallPrint?: string;
miniCta?: MiniCta;
}
const socialIcons = {
twitter: Twitter,
linkedin: Linkedin,
github: Github,
facebook: Facebook,
instagram: Instagram,
youtube: Youtube,
};
const FooterRender: React.FC<FooterProps> = ({
brandText,
brandLogo,
description,
columns,
socialLinks,
smallPrint,
miniCta,
}) => {
const hasSocialLinks = socialLinks && Object.values(socialLinks).some(Boolean);
return (
<footer className="bg-neutral-900 text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className="grid grid-cols-1 lg:grid-cols-6 gap-12">
{/* Brand column */}
<div className="lg:col-span-2">
{brandLogo ? (
<img src={brandLogo} alt={brandText} className="h-8 w-auto" />
) : (
<span className="text-xl font-bold">{brandText}</span>
)}
{description && (
<p className="mt-4 text-neutral-400 text-sm leading-relaxed">
{description}
</p>
)}
{hasSocialLinks && (
<div className="mt-6 flex gap-4">
{Object.entries(socialLinks || {}).map(([key, url]) => {
if (!url) return null;
const Icon = socialIcons[key as keyof typeof socialIcons];
if (!Icon) return null;
return (
<a
key={key}
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-neutral-400 hover:text-white transition-colors"
aria-label={key}
>
<Icon className="w-5 h-5" />
</a>
);
})}
</div>
)}
</div>
{/* Link columns */}
{columns.map((column, index) => (
<div key={index}>
<h3 className="text-sm font-semibold text-white uppercase tracking-wider">
{column.title}
</h3>
<ul className="mt-4 space-y-3">
{column.links.map((link, linkIndex) => (
<li key={linkIndex}>
<a
href={link.href}
className="text-neutral-400 hover:text-white text-sm transition-colors"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
))}
</div>
{/* Mini CTA (newsletter) */}
{miniCta && (
<div className="mt-12 pt-8 border-t border-neutral-800">
<div className="max-w-md">
<p className="text-sm font-semibold text-white">{miniCta.text}</p>
<div className="mt-4 flex gap-3">
<input
type="email"
placeholder={miniCta.placeholder}
className="flex-1 px-4 py-2 bg-neutral-800 border border-neutral-700 rounded-lg text-white placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
<button className="px-6 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 transition-colors">
{miniCta.buttonText}
</button>
</div>
</div>
</div>
)}
{/* Small print */}
{smallPrint && (
<div className="mt-12 pt-8 border-t border-neutral-800">
<p className="text-neutral-500 text-sm">{smallPrint}</p>
</div>
)}
</div>
</footer>
);
};
export const Footer: ComponentConfig<FooterProps> = {
label: 'Footer',
fields: {
brandText: {
type: 'text',
label: 'Brand Name',
},
brandLogo: {
type: 'text',
label: 'Brand Logo URL',
},
description: {
type: 'textarea',
label: 'Description',
},
columns: {
type: 'array',
label: 'Link Columns',
arrayFields: {
title: { type: 'text', label: 'Column Title' },
links: {
type: 'array',
label: 'Links',
arrayFields: {
label: { type: 'text', label: 'Label' },
href: { type: 'text', label: 'URL' },
},
},
},
defaultItemProps: {
title: 'Column',
links: [
{ label: 'Link 1', href: '#' },
{ label: 'Link 2', href: '#' },
],
},
},
socialLinks: {
type: 'object',
label: 'Social Links',
objectFields: {
twitter: { type: 'text', label: 'Twitter URL' },
linkedin: { type: 'text', label: 'LinkedIn URL' },
github: { type: 'text', label: 'GitHub URL' },
facebook: { type: 'text', label: 'Facebook URL' },
instagram: { type: 'text', label: 'Instagram URL' },
youtube: { type: 'text', label: 'YouTube URL' },
},
},
smallPrint: {
type: 'text',
label: 'Copyright/Small Print',
},
miniCta: {
type: 'object',
label: 'Newsletter CTA (optional)',
objectFields: {
text: { type: 'text', label: 'Heading' },
placeholder: { type: 'text', label: 'Placeholder' },
buttonText: { type: 'text', label: 'Button Text' },
},
},
},
defaultProps: {
brandText: 'Your Business',
description: 'Add a brief description of your business here.',
columns: [
{
title: 'Navigation',
links: [
{ label: 'Home', href: '#' },
{ label: 'About', href: '#' },
{ label: 'Services', href: '#' },
],
},
{
title: 'Contact',
links: [
{ label: 'Contact Us', href: '#' },
{ label: 'Support', href: '#' },
{ label: 'Location', href: '#' },
],
},
{
title: 'Legal',
links: [
{ label: 'Privacy Policy', href: '#' },
{ label: 'Terms of Service', href: '#' },
],
},
],
socialLinks: {},
smallPrint: '© 2024 Your Business. All rights reserved.',
},
render: FooterRender,
};
export default Footer;