Files
smoothschedule/frontend/src/puck/components/contact/Map.tsx
poduck 29bcb27e76 Add Puck site builder with preview and draft functionality
Frontend:
- Add comprehensive Puck component library (Layout, Content, Booking, Contact)
- Add Services component with usePublicServices hook integration
- Add 150+ icons to IconList component organized by category
- Add preview modal with viewport toggles (desktop/tablet/mobile)
- Add draft save/discard functionality with localStorage persistence
- Add draft status indicator in PageEditor toolbar
- Fix useSites hooks to use correct API URLs (/pages/{id}/)

Backend:
- Add SiteConfig model for theme, header, footer configuration
- Add Page SEO fields (meta_title, meta_description, og_image, etc.)
- Add puck_data validation for component structure
- Add create_missing_sites management command
- Fix PageViewSet to use EntitlementService for permissions
- Add comprehensive tests for site builder functionality

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 01:32:11 -05:00

92 lines
2.6 KiB
TypeScript

import React from 'react';
import type { ComponentConfig } from '@measured/puck';
import type { MapProps } from '../../types';
import { MapPin, AlertTriangle } from 'lucide-react';
// Allowlisted embed domains for security
const ALLOWED_EMBED_DOMAINS = [
'www.google.com/maps/embed',
'maps.google.com',
'www.openstreetmap.org',
];
function isAllowedEmbed(url: string): boolean {
if (!url) return false;
if (!url.startsWith('https://')) return false;
return ALLOWED_EMBED_DOMAINS.some((domain) =>
url.startsWith(`https://${domain}`)
);
}
export const Map: ComponentConfig<MapProps> = {
label: 'Map',
fields: {
embedUrl: {
type: 'text',
label: 'Google Maps Embed URL',
},
height: {
type: 'number',
label: 'Height (px)',
},
},
defaultProps: {
embedUrl: '',
height: 400,
},
render: ({ embedUrl, height }) => {
// Validate embed URL
if (!embedUrl) {
return (
<div
className="bg-gray-100 dark:bg-gray-800 rounded-lg flex flex-col items-center justify-center"
style={{ height: `${height}px` }}
>
<MapPin className="w-12 h-12 text-gray-400 mb-4" />
<p className="text-gray-500 dark:text-gray-400 text-center">
Add a Google Maps embed URL to display a map
</p>
<p className="text-sm text-gray-400 dark:text-gray-500 mt-2 text-center max-w-md">
Go to Google Maps, search for your location, click "Share" "Embed a map" and copy the src URL from the iframe code.
</p>
</div>
);
}
if (!isAllowedEmbed(embedUrl)) {
return (
<div
className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex flex-col items-center justify-center"
style={{ height: `${height}px` }}
>
<AlertTriangle className="w-12 h-12 text-red-400 mb-4" />
<p className="text-red-600 dark:text-red-400 text-center font-medium">
Invalid embed URL
</p>
<p className="text-sm text-red-500 dark:text-red-400/80 mt-2 text-center max-w-md">
Only Google Maps and OpenStreetMap embeds are allowed for security reasons.
</p>
</div>
);
}
return (
<div className="rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700">
<iframe
src={embedUrl}
width="100%"
height={height}
style={{ border: 0 }}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
title="Location Map"
/>
</div>
);
},
};
export default Map;