Files
smoothschedule/frontend/src/puck/components/contact/AddressBlock.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

216 lines
6.0 KiB
TypeScript

import React from 'react';
import type { ComponentConfig } from '@measured/puck';
import { MapPin, Phone, Mail, Building2 } from 'lucide-react';
export interface AddressBlockProps {
businessName?: string;
address?: string;
address2?: string;
city?: string;
state?: string;
zip?: string;
phone?: string;
email?: string;
showIcons: boolean;
layout: 'vertical' | 'horizontal';
alignment: 'left' | 'center' | 'right';
}
const formatPhone = (phone: string): string => {
// Remove all non-digits
const digits = phone.replace(/\D/g, '');
// Format as (XXX) XXX-XXXX if 10 digits
if (digits.length === 10) {
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
}
// Return original if not 10 digits
return phone;
};
const AddressBlockRender: React.FC<AddressBlockProps> = ({
businessName,
address,
address2,
city,
state,
zip,
phone,
email,
showIcons = true,
layout = 'vertical',
alignment = 'left',
}) => {
const alignmentClasses = {
left: 'text-left',
center: 'text-center',
right: 'text-right',
};
const justifyClasses = {
left: 'justify-start',
center: 'justify-center',
right: 'justify-end',
};
// Build full address string
const addressParts = [];
if (address) addressParts.push(address);
if (address2) addressParts.push(address2);
const cityStateZip = [city, state].filter(Boolean).join(', ');
const fullCityLine = [cityStateZip, zip].filter(Boolean).join(' ');
if (fullCityLine) addressParts.push(fullCityLine);
const fullAddress = addressParts.join(', ');
const items = [
{ icon: Building2, value: businessName, href: null },
{ icon: MapPin, value: fullAddress, href: fullAddress ? `https://maps.google.com/?q=${encodeURIComponent(fullAddress)}` : null },
{ icon: Phone, value: phone ? formatPhone(phone) : null, href: phone ? `tel:${phone.replace(/\D/g, '')}` : null },
{ icon: Mail, value: email, href: email ? `mailto:${email}` : null },
].filter(item => item.value);
if (items.length === 0) {
return (
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 text-center text-gray-500">
Add your business contact information
</div>
);
}
if (layout === 'horizontal') {
return (
<div className={`bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6`}>
<div className={`flex flex-wrap gap-6 ${justifyClasses[alignment]}`}>
{items.map(({ icon: Icon, value, href }, index) => (
<div key={index} className="flex items-center gap-2">
{showIcons && (
<Icon className="w-5 h-5 text-primary-600 dark:text-primary-400 flex-shrink-0" />
)}
{href ? (
<a
href={href}
target={href.startsWith('https') ? '_blank' : undefined}
rel={href.startsWith('https') ? 'noopener noreferrer' : undefined}
className="text-gray-700 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
>
{value}
</a>
) : (
<span className="text-gray-900 dark:text-white font-medium">{value}</span>
)}
</div>
))}
</div>
</div>
);
}
return (
<div className={`bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 ${alignmentClasses[alignment]}`}>
<div className={`space-y-4 ${alignment === 'center' ? 'flex flex-col items-center' : ''}`}>
{items.map(({ icon: Icon, value, href }, index) => (
<div key={index} className={`flex items-start gap-3 ${justifyClasses[alignment]}`}>
{showIcons && (
<Icon className="w-5 h-5 text-primary-600 dark:text-primary-400 flex-shrink-0 mt-0.5" />
)}
{href ? (
<a
href={href}
target={href.startsWith('https') ? '_blank' : undefined}
rel={href.startsWith('https') ? 'noopener noreferrer' : undefined}
className="text-gray-700 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
>
{value}
</a>
) : (
<span className="text-gray-900 dark:text-white font-semibold text-lg">{value}</span>
)}
</div>
))}
</div>
</div>
);
};
export const AddressBlock: ComponentConfig<AddressBlockProps> = {
label: 'Address Block',
fields: {
businessName: {
type: 'text',
label: 'Business Name',
},
address: {
type: 'text',
label: 'Street Address',
},
address2: {
type: 'text',
label: 'Address Line 2 (Suite, Unit, etc.)',
},
city: {
type: 'text',
label: 'City',
},
state: {
type: 'text',
label: 'State',
},
zip: {
type: 'text',
label: 'ZIP Code',
},
phone: {
type: 'text',
label: 'Phone Number',
},
email: {
type: 'text',
label: 'Email Address',
},
showIcons: {
type: 'radio',
label: 'Show Icons',
options: [
{ label: 'Yes', value: true },
{ label: 'No', value: false },
],
},
layout: {
type: 'select',
label: 'Layout',
options: [
{ label: 'Vertical (Stacked)', value: 'vertical' },
{ label: 'Horizontal (Inline)', value: 'horizontal' },
],
},
alignment: {
type: 'select',
label: 'Alignment',
options: [
{ label: 'Left', value: 'left' },
{ label: 'Center', value: 'center' },
{ label: 'Right', value: 'right' },
],
},
},
defaultProps: {
businessName: '',
address: '',
address2: '',
city: '',
state: '',
zip: '',
phone: '',
email: '',
showIcons: true,
layout: 'vertical',
alignment: 'left',
},
render: AddressBlockRender,
};
export default AddressBlock;