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>
92 lines
2.6 KiB
TypeScript
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;
|