- Add geolocation Tiptap extension for embedding maps - Create GeolocationExtended component for rendering map embeds - Create GeolocationPlaceholder for editor insertion - Add GeoCard component for displaying location data - Support latitude, longitude, zoom level, and optional labels - Enable location-based content in albums and posts Allows editors to embed interactive maps with specific locations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
255 lines
4.7 KiB
Svelte
255 lines
4.7 KiB
Svelte
<script lang="ts">
|
|
import { type Editor, type NodeViewProps } from '@tiptap/core'
|
|
import { NodeViewWrapper } from 'svelte-tiptap'
|
|
import MapPin from 'lucide-svelte/icons/map-pin'
|
|
|
|
interface Props extends NodeViewProps {}
|
|
let { node, editor, getPos, updateAttributes, deleteNode }: Props = $props()
|
|
|
|
let latitude = $state<number | null>(null)
|
|
let longitude = $state<number | null>(null)
|
|
let title = $state('')
|
|
let description = $state('')
|
|
let markerColor = $state('#ef4444')
|
|
let isConfigured = $state(false)
|
|
|
|
function handleSubmit() {
|
|
if (!latitude || !longitude || !title) {
|
|
alert('Please fill in all required fields')
|
|
return
|
|
}
|
|
|
|
// Replace this placeholder with the actual geolocation node
|
|
const pos = getPos()
|
|
if (typeof pos === 'number') {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange({ from: pos, to: pos + node.nodeSize })
|
|
.insertContent({
|
|
type: 'geolocation',
|
|
attrs: {
|
|
latitude,
|
|
longitude,
|
|
title,
|
|
description,
|
|
markerColor
|
|
}
|
|
})
|
|
.run()
|
|
}
|
|
}
|
|
|
|
function handleCancel() {
|
|
deleteNode()
|
|
}
|
|
</script>
|
|
|
|
<NodeViewWrapper>
|
|
<div class="geolocation-placeholder">
|
|
<div class="icon">
|
|
<MapPin size={24} />
|
|
</div>
|
|
|
|
{#if !isConfigured}
|
|
<div class="content">
|
|
<h3>Add Location</h3>
|
|
<p>Add a map with a location marker</p>
|
|
<button class="configure-btn" onclick={() => (isConfigured = true)}>
|
|
Configure Location
|
|
</button>
|
|
</div>
|
|
{:else}
|
|
<div class="form">
|
|
<h3>Configure Location</h3>
|
|
|
|
<div class="form-group">
|
|
<label for="latitude">Latitude <span class="required">*</span></label>
|
|
<input
|
|
id="latitude"
|
|
type="number"
|
|
step="0.000001"
|
|
bind:value={latitude}
|
|
placeholder="e.g., 37.7749"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="longitude">Longitude <span class="required">*</span></label>
|
|
<input
|
|
id="longitude"
|
|
type="number"
|
|
step="0.000001"
|
|
bind:value={longitude}
|
|
placeholder="e.g., -122.4194"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="title">Title <span class="required">*</span></label>
|
|
<input id="title" type="text" bind:value={title} placeholder="Location name" required />
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="description">Description</label>
|
|
<textarea
|
|
id="description"
|
|
bind:value={description}
|
|
placeholder="Optional description"
|
|
rows="3"
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="markerColor">Marker Color</label>
|
|
<input id="markerColor" type="color" bind:value={markerColor} />
|
|
</div>
|
|
|
|
<div class="actions">
|
|
<button class="cancel-btn" onclick={handleCancel}>Cancel</button>
|
|
<button class="submit-btn" onclick={handleSubmit}>Add Location</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</NodeViewWrapper>
|
|
|
|
<style lang="scss">
|
|
.geolocation-placeholder {
|
|
background: #f8f9fa;
|
|
border: 2px dashed #e0e0e0;
|
|
border-radius: 8px;
|
|
padding: 24px;
|
|
margin: 16px 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.icon {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-bottom: 16px;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.content {
|
|
h3 {
|
|
margin: 0 0 8px;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #1f2937;
|
|
}
|
|
|
|
p {
|
|
margin: 0 0 16px;
|
|
color: #6b7280;
|
|
}
|
|
}
|
|
|
|
.configure-btn {
|
|
background: #3b82f6;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 8px 16px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
|
|
&:hover {
|
|
background: #2563eb;
|
|
}
|
|
}
|
|
|
|
.form {
|
|
text-align: left;
|
|
max-width: 400px;
|
|
margin: 0 auto;
|
|
|
|
h3 {
|
|
margin: 0 0 20px;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #1f2937;
|
|
text-align: center;
|
|
}
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 16px;
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 4px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #374151;
|
|
}
|
|
|
|
.required {
|
|
color: #ef4444;
|
|
}
|
|
|
|
input,
|
|
textarea {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
font-family: inherit;
|
|
box-sizing: border-box;
|
|
|
|
&:focus {
|
|
outline: none;
|
|
border-color: #3b82f6;
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
}
|
|
}
|
|
|
|
input[type='color'] {
|
|
width: 60px;
|
|
height: 36px;
|
|
padding: 4px;
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
justify-content: flex-end;
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.cancel-btn,
|
|
.submit-btn {
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
border: none;
|
|
}
|
|
|
|
.cancel-btn {
|
|
background: #f3f4f6;
|
|
color: #374151;
|
|
|
|
&:hover {
|
|
background: #e5e7eb;
|
|
}
|
|
}
|
|
|
|
.submit-btn {
|
|
background: #3b82f6;
|
|
color: white;
|
|
|
|
&:hover {
|
|
background: #2563eb;
|
|
}
|
|
}
|
|
</style>
|