fixed map embeds to include directions, substack embeds, twitter embeds
This commit is contained in:
parent
3515bce049
commit
7a1093b12a
|
|
@ -1,6 +1,7 @@
|
||||||
# Google API Credentials
|
# Google API Credentials
|
||||||
VITE_GOOGLE_CLIENT_ID='your_google_client_id'
|
VITE_GOOGLE_CLIENT_ID='your_google_client_id'
|
||||||
VITE_GOOGLE_API_KEY='your_google_api_key'
|
VITE_GOOGLE_API_KEY='your_google_api_key'
|
||||||
|
VITE_GOOGLE_MAPS_API_KEY='your_google_maps_api_key'
|
||||||
|
|
||||||
# Cloudflare Worker
|
# Cloudflare Worker
|
||||||
CLOUDFLARE_API_TOKEN='your_cloudflare_token'
|
CLOUDFLARE_API_TOKEN='your_cloudflare_token'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"
|
import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"
|
||||||
import { useCallback, useState } from "react"
|
import { useCallback, useState } from "react"
|
||||||
|
//import Embed from "react-embed"
|
||||||
|
|
||||||
export type IEmbedShape = TLBaseShape<
|
export type IEmbedShape = TLBaseShape<
|
||||||
"Embed",
|
"Embed",
|
||||||
|
|
@ -10,6 +11,69 @@ export type IEmbedShape = TLBaseShape<
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
const transformUrl = (url: string): string => {
|
||||||
|
// YouTube
|
||||||
|
const youtubeMatch = url.match(
|
||||||
|
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/,
|
||||||
|
)
|
||||||
|
if (youtubeMatch) {
|
||||||
|
return `https://www.youtube.com/embed/${youtubeMatch[1]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google Maps
|
||||||
|
if (url.includes("google.com/maps") || url.includes("goo.gl/maps")) {
|
||||||
|
// If it's already an embed URL, return as is
|
||||||
|
if (url.includes("google.com/maps/embed")) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle directions
|
||||||
|
const directionsMatch = url.match(/dir\/([^\/]+)\/([^\/]+)/)
|
||||||
|
if (directionsMatch || url.includes("/dir/")) {
|
||||||
|
const origin = url.match(/origin=([^&]+)/)?.[1] || directionsMatch?.[1]
|
||||||
|
const destination =
|
||||||
|
url.match(/destination=([^&]+)/)?.[1] || directionsMatch?.[2]
|
||||||
|
|
||||||
|
if (origin && destination) {
|
||||||
|
return `https://www.google.com/maps/embed/v1/directions?key=${
|
||||||
|
import.meta.env.VITE_GOOGLE_MAPS_API_KEY
|
||||||
|
}&origin=${encodeURIComponent(origin)}&destination=${encodeURIComponent(
|
||||||
|
destination,
|
||||||
|
)}&mode=driving`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract place ID
|
||||||
|
const placeMatch = url.match(/[?&]place_id=([^&]+)/)
|
||||||
|
if (placeMatch) {
|
||||||
|
return `https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2!2d0!3d0!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s${placeMatch[1]}!2s!5e0!3m2!1sen!2s!4v1`
|
||||||
|
}
|
||||||
|
|
||||||
|
// For all other map URLs
|
||||||
|
return `https://www.google.com/maps/embed/v1/place?key=${
|
||||||
|
import.meta.env.VITE_GOOGLE_MAPS_API_KEY
|
||||||
|
}&q=${encodeURIComponent(url)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Twitter/X
|
||||||
|
const tweetMatch = url.match(/(?:twitter\.com|x\.com)\/[^\/]+\/status\/(\d+)/)
|
||||||
|
if (tweetMatch) {
|
||||||
|
return `https://platform.x.com/embed/Tweet.html?id=${tweetMatch[1]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Medium
|
||||||
|
if (url.includes("medium.com")) {
|
||||||
|
return url.replace(/\/?$/, "?format=lite")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather.town
|
||||||
|
if (url.includes("app.gather.town")) {
|
||||||
|
return url.replace("app.gather.town", "gather.town/embed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
|
export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
|
||||||
static override type = "Embed"
|
static override type = "Embed"
|
||||||
|
|
||||||
|
|
@ -41,29 +105,11 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
|
||||||
? inputUrl
|
? inputUrl
|
||||||
: `https://${inputUrl}`
|
: `https://${inputUrl}`
|
||||||
|
|
||||||
// Handle YouTube links
|
// Basic URL validation
|
||||||
if (
|
const isValidUrl = completedUrl.match(/(^\w+:|^)\/\//)
|
||||||
completedUrl.includes("youtube.com") ||
|
if (!isValidUrl) {
|
||||||
completedUrl.includes("youtu.be")
|
setError("Invalid URL")
|
||||||
) {
|
return
|
||||||
const videoId = extractYouTubeVideoId(completedUrl)
|
|
||||||
if (videoId) {
|
|
||||||
completedUrl = `https://www.youtube.com/embed/${videoId}`
|
|
||||||
} else {
|
|
||||||
setError("Invalid YouTube URL")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Google Docs links
|
|
||||||
if (completedUrl.includes("docs.google.com")) {
|
|
||||||
const docId = completedUrl.match(/\/d\/([a-zA-Z0-9-_]+)/)?.[1]
|
|
||||||
if (docId) {
|
|
||||||
completedUrl = `https://docs.google.com/document/d/${docId}/preview`
|
|
||||||
} else {
|
|
||||||
setError("Invalid Google Docs URL")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editor.updateShape<IEmbedShape>({
|
this.editor.updateShape<IEmbedShape>({
|
||||||
|
|
@ -71,25 +117,11 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
|
||||||
type: "Embed",
|
type: "Embed",
|
||||||
props: { ...shape.props, url: completedUrl },
|
props: { ...shape.props, url: completedUrl },
|
||||||
})
|
})
|
||||||
|
setError("")
|
||||||
// Check if the URL is valid
|
|
||||||
const isValidUrl = completedUrl.match(/(^\w+:|^)\/\//)
|
|
||||||
if (!isValidUrl) {
|
|
||||||
setError("Invalid website URL")
|
|
||||||
} else {
|
|
||||||
setError("")
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[inputUrl],
|
[inputUrl],
|
||||||
)
|
)
|
||||||
|
|
||||||
const extractYouTubeVideoId = (url: string): string | null => {
|
|
||||||
const regExp =
|
|
||||||
/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/
|
|
||||||
const match = url.match(regExp)
|
|
||||||
return match && match[2].length === 11 ? match[2] : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapperStyle = {
|
const wrapperStyle = {
|
||||||
width: `${shape.props.w}px`,
|
width: `${shape.props.w}px`,
|
||||||
height: `${shape.props.h}px`,
|
height: `${shape.props.h}px`,
|
||||||
|
|
@ -152,10 +184,11 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
|
||||||
<div style={wrapperStyle}>
|
<div style={wrapperStyle}>
|
||||||
<div style={contentStyle}>
|
<div style={contentStyle}>
|
||||||
<iframe
|
<iframe
|
||||||
src={shape.props.url}
|
src={transformUrl(shape.props.url)}
|
||||||
width="100%"
|
width={shape.props.w}
|
||||||
height="100%"
|
height={shape.props.h}
|
||||||
style={{ border: "none" }}
|
style={{ border: "none" }}
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_TLDRAW_WORKER_URL: string
|
readonly VITE_TLDRAW_WORKER_URL: string
|
||||||
|
readonly VITE_GOOGLE_MAPS_API_KEY: string
|
||||||
|
readonly VITE_DAILY_API_KEY: string
|
||||||
|
readonly VITE_CLOUDFLARE_API_TOKEN: string
|
||||||
|
readonly VITE_CLOUDFLARE_ACCOUNT_ID: string
|
||||||
|
readonly VITE_CLOUDFLARE_ZONE_ID: string
|
||||||
|
readonly VITE_R2_BUCKET_NAME: string
|
||||||
|
readonly VITE_R2_PREVIEW_BUCKET_NAME: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv
|
readonly env: ImportMetaEnv
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue