fixed map embeds to include directions, substack embeds, twitter embeds

This commit is contained in:
Jeff Emmett 2024-12-09 18:55:38 -05:00
parent 3515bce049
commit 7a1093b12a
3 changed files with 84 additions and 43 deletions

View File

@ -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'

View File

@ -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>

11
src/vite-env.d.ts vendored
View File

@ -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
} }