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>
249 lines
6.9 KiB
TypeScript
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;
|