/ src / components / embed / embed.tsx
embed.tsx
  1  import styles from './embed.module.css';
  2  
  3  interface EmbedProps {
  4    url: string;
  5  }
  6  
  7  const Embed = ({ url }: EmbedProps) => {
  8    const parsedUrl = new URL(url);
  9  
 10    if (youtubeHosts.has(parsedUrl.host) || (parsedUrl.host.startsWith('yt.') && parsedUrl.searchParams.has('v'))) {
 11      return <YoutubeEmbed parsedUrl={parsedUrl} />;
 12    }
 13    if (xHosts.has(parsedUrl.host)) {
 14      return <XEmbed parsedUrl={parsedUrl} />;
 15    }
 16    if (redditHosts.has(parsedUrl.host)) {
 17      return <RedditEmbed parsedUrl={parsedUrl} />;
 18    }
 19    if (twitchHosts.has(parsedUrl.host)) {
 20      return <TwitchEmbed parsedUrl={parsedUrl} />;
 21    }
 22    if (tiktokHosts.has(parsedUrl.host)) {
 23      return <TiktokEmbed parsedUrl={parsedUrl} />;
 24    }
 25    if (instagramHosts.has(parsedUrl.host)) {
 26      return <InstagramEmbed parsedUrl={parsedUrl} />;
 27    }
 28    if (odyseeHosts.has(parsedUrl.host)) {
 29      return <OdyseeEmbed parsedUrl={parsedUrl} />;
 30    }
 31    if (bitchuteHosts.has(parsedUrl.host)) {
 32      return <BitchuteEmbed parsedUrl={parsedUrl} />;
 33    }
 34    if (streamableHosts.has(parsedUrl.host)) {
 35      return <StreamableEmbed parsedUrl={parsedUrl} />;
 36    }
 37    if (spotifyHosts.has(parsedUrl.host)) {
 38      return <SpotifyEmbed parsedUrl={parsedUrl} />;
 39    }
 40    if (soundcloudHosts.has(parsedUrl.host)) {
 41      return <SoundcloudEmbed parsedUrl={parsedUrl} />;
 42    }
 43  };
 44  
 45  interface EmbedComponentProps {
 46    parsedUrl: URL;
 47  }
 48  
 49  const youtubeHosts = new Set<string>(['youtube.com', 'www.youtube.com', 'youtu.be', 'www.youtu.be', 'm.youtube.com', 'music.youtube.com']);
 50  
 51  const YoutubeEmbed = ({ parsedUrl }: EmbedComponentProps) => {
 52    let embedSrc = '';
 53  
 54    if (parsedUrl.searchParams.has('list')) {
 55      const playlistId = parsedUrl.searchParams.get('list');
 56      embedSrc = `https://www.youtube.com/embed/videoseries?list=${playlistId}`;
 57    } else {
 58      let videoId = parsedUrl.searchParams.get('v');
 59  
 60      if (!videoId && parsedUrl.host.includes('youtu.be')) {
 61        videoId = parsedUrl.pathname.substring(1);
 62      }
 63  
 64      if (videoId) {
 65        embedSrc = `https://www.youtube.com/embed/${videoId}`;
 66      }
 67    }
 68  
 69    if (embedSrc) {
 70      return (
 71        <iframe
 72          className={styles.videoEmbed}
 73          height='100%'
 74          width='100%'
 75          referrerPolicy='origin'
 76          allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'
 77          allowFullScreen
 78          title={parsedUrl.href}
 79          src={embedSrc}
 80        />
 81      );
 82    }
 83    return null;
 84  };
 85  
 86  const xHosts = new Set<string>(['twitter.com', 'www.twitter.com', 'x.com', 'www.x.com']);
 87  
 88  const XEmbed = ({ parsedUrl }: EmbedComponentProps) => {
 89    return (
 90      <iframe
 91        className={styles.xEmbed}
 92        height='100%'
 93        width='100%'
 94        referrerPolicy='no-referrer'
 95        allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
 96        title={parsedUrl.href}
 97        srcDoc={`
 98        <blockquote class="twitter-tweet" data-theme="dark">
 99          <a href="${parsedUrl.href.replace('x.com', 'twitter.com')}"></a>
100        </blockquote>
101        <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
102      `}
103      />
104    );
105  };
106  
107  const redditHosts = new Set<string>(['reddit.com', 'www.reddit.com', 'old.reddit.com']);
108  
109  const RedditEmbed = ({ parsedUrl }: EmbedComponentProps) => {
110    return (
111      <iframe
112        className={styles.redditEmbed}
113        height='100%'
114        width='100%'
115        referrerPolicy='no-referrer'
116        allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
117        title={parsedUrl.href}
118        srcDoc={`
119        <style>
120          /* fix reddit iframe being centered */
121          iframe {
122            margin: 0!important;
123          }
124        </style>
125        <blockquote class="reddit-embed-bq" style="height:240px" data-embed-theme="dark">
126          <a href="${parsedUrl.href}"></a>    
127        </blockquote>
128        <script async src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>
129      `}
130      />
131    );
132  };
133  
134  const twitchHosts = new Set<string>(['twitch.tv', 'www.twitch.tv']);
135  
136  const TwitchEmbed = ({ parsedUrl }: EmbedComponentProps) => {
137    let iframeUrl;
138    if (parsedUrl.pathname.startsWith('/videos/')) {
139      const videoId = parsedUrl.pathname.replace('/videos/', '');
140      iframeUrl = `https://player.twitch.tv/?video=${videoId}&parent=${window.location.hostname}`;
141    } else {
142      const channel = parsedUrl.pathname.replaceAll('/', '');
143      iframeUrl = `https://player.twitch.tv/?channel=${channel}&parent=${window.location.hostname}`;
144    }
145    return (
146      <iframe
147        className={styles.videoEmbed}
148        height='100%'
149        width='100%'
150        referrerPolicy='no-referrer'
151        allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
152        allowFullScreen
153        title={parsedUrl.href}
154        src={iframeUrl}
155      />
156    );
157  };
158  
159  const tiktokHosts = new Set<string>(['tiktok.com', 'www.tiktok.com']);
160  
161  const TiktokEmbed = ({ parsedUrl }: EmbedComponentProps) => {
162    const videoId = parsedUrl.pathname.replace(/.+\/video\//, '').replaceAll('/', '');
163    return (
164      <iframe
165        className={styles.tiktokEmbed}
166        height='100%'
167        width='100%'
168        referrerPolicy='no-referrer'
169        allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
170        title={parsedUrl.href}
171        srcDoc={`
172        <blockquote class="tiktok-embed" data-video-id="${videoId}">
173          <a></a>
174        </blockquote> 
175        <script async src="https://www.tiktok.com/embed.js"></script>
176      `}
177      />
178    );
179  };
180  
181  const instagramHosts = new Set<string>(['instagram.com', 'www.instagram.com']);
182  
183  const InstagramEmbed = ({ parsedUrl }: EmbedComponentProps) => {
184    const pathNames = parsedUrl.pathname.replace(/\/+$/, '').split('/');
185    const id = pathNames[pathNames.length - 1];
186    return (
187      <iframe
188        className={styles.instagramEmbed}
189        height='100%'
190        width='100%'
191        referrerPolicy='no-referrer'
192        allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
193        title={parsedUrl.href}
194        srcDoc={`
195        <blockquote class="instagram-media">
196          <a href="https://www.instagram.com/p/${id}/"></a>
197        </blockquote>
198        <script async src="//www.instagram.com/embed.js"></script>
199      `}
200      />
201    );
202  };
203  
204  const odyseeHosts = new Set<string>(['odysee.com', 'www.odysee.com']);
205  
206  const OdyseeEmbed = ({ parsedUrl }: EmbedComponentProps) => {
207    const iframeUrl = `https://odysee.com/$/embed${parsedUrl.pathname}`;
208    return (
209      <iframe
210        className={styles.videoEmbed}
211        height='100%'
212        width='100%'
213        referrerPolicy='no-referrer'
214        allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
215        allowFullScreen
216        title={parsedUrl.href}
217        src={iframeUrl}
218      />
219    );
220  };
221  
222  const bitchuteHosts = new Set<string>(['bitchute.com', 'www.bitchute.com']);
223  
224  const BitchuteEmbed = ({ parsedUrl }: EmbedComponentProps) => {
225    const videoId = parsedUrl.pathname.replace(/\/video\//, '').replaceAll('/', '');
226    return (
227      <iframe
228        className={styles.videoEmbed}
229        height='100%'
230        width='100%'
231        referrerPolicy='no-referrer'
232        allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
233        allowFullScreen
234        title={parsedUrl.href}
235        src={`https://www.bitchute.com/embed/${videoId}/`}
236      />
237    );
238  };
239  
240  const streamableHosts = new Set<string>(['streamable.com', 'www.streamable.com']);
241  
242  const StreamableEmbed = ({ parsedUrl }: EmbedComponentProps) => {
243    const videoId = parsedUrl.pathname.replaceAll('/', '');
244    return (
245      <iframe
246        className={styles.videoEmbed}
247        height='100%'
248        width='100%'
249        referrerPolicy='no-referrer'
250        allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
251        allowFullScreen
252        title={parsedUrl.href}
253        src={`https://streamable.com/e/${videoId}`}
254      />
255    );
256  };
257  
258  const spotifyHosts = new Set<string>(['spotify.com', 'www.spotify.com', 'open.spotify.com']);
259  
260  const SpotifyEmbed = ({ parsedUrl }: EmbedComponentProps) => {
261    const iframeUrl = `https://open.spotify.com/embed${parsedUrl.pathname}?theme=0`;
262    return (
263      <iframe
264        className={styles.audioEmbed}
265        height='100%'
266        width='100%'
267        referrerPolicy='no-referrer'
268        allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
269        allowFullScreen
270        title={parsedUrl.href}
271        src={iframeUrl}
272      />
273    );
274  };
275  
276  const soundcloudHosts = new Set(['soundcloud.com', 'www.soundcloud.com', 'on.soundcloud.com', 'api.soundcloud.com', 'w.soundcloud.com']);
277  
278  // not officially documented https://stackoverflow.com/questions/20870270/how-to-get-soundcloud-embed-code-by-soundcloud-com-url
279  const SoundcloudEmbed = ({ parsedUrl }: EmbedComponentProps) => {
280    return (
281      <iframe
282        className={styles.soundcloudEmbed}
283        height='100%'
284        width='100%'
285        referrerPolicy='no-referrer'
286        allow='accelerometer; encrypted-media; gyroscope; picture-in-picture; web-share'
287        allowFullScreen
288        title={parsedUrl.href}
289        src={`https://w.soundcloud.com/player/?url=${parsedUrl.href}`}
290      />
291    );
292  };
293  
294  const canEmbedHosts = new Set<string>([
295    ...youtubeHosts,
296    ...xHosts,
297    ...redditHosts,
298    ...twitchHosts,
299    ...tiktokHosts,
300    ...instagramHosts,
301    ...odyseeHosts,
302    ...bitchuteHosts,
303    ...soundcloudHosts,
304    ...streamableHosts,
305    ...spotifyHosts,
306  ]);
307  
308  export const canEmbed = (parsedUrl: URL): boolean => {
309    return canEmbedHosts.has(parsedUrl.host) || (parsedUrl.host.startsWith('yt.') && parsedUrl.searchParams.has('v'));
310  };
311  
312  export default Embed;