Implement Youtube embeds

This commit is contained in:
Justin Edmund 2022-12-26 09:48:12 -08:00
parent 5bdac9624d
commit 797215eeff
2 changed files with 252 additions and 124 deletions

View file

@ -1,23 +1,23 @@
.PartyDetails { .DetailsWrapper {
display: none; // This breaks transition, find a workaround
opacity: 0;
margin: $unit-4x auto 0;
max-width: $unit * 94;
position: relative;
&.Editable {
top: $unit;
height: 0;
z-index: 2;
transition: opacity 0.2s ease-in-out, top 0.2s ease-in-out;
&.Visible {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.PartyDetails {
display: none;
margin: 0 auto;
max-width: $unit * 94;
overflow: hidden;
width: 100%;
&.Visible {
margin-bottom: $unit-12x;
}
&.Editable {
gap: $unit; gap: $unit;
height: auto;
opacity: 1; &.Visible {
top: 0; display: grid;
} }
fieldset { fieldset {
@ -30,11 +30,14 @@
} }
} }
.SelectTrigger {
width: 100%;
}
.bottom { .bottom {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: $unit; gap: $unit;
margin-bottom: $unit-12x;
.left { .left {
flex-grow: 1; flex-grow: 1;
@ -49,14 +52,8 @@
} }
&.ReadOnly { &.ReadOnly {
top: $unit * -1;
transition: opacity 0.2s ease-in-out, top 0.2s ease-in-out;
&.Visible { &.Visible {
display: block; display: block;
height: auto;
opacity: 1;
top: 0;
} }
a:hover { a:hover {
@ -69,32 +66,111 @@
white-space: pre-line; white-space: pre-line;
} }
.YoutubeWrapper {
background-color: var(--card-bg);
border-radius: $card-corner;
margin: $unit 0;
position: relative;
display: block;
contain: content;
background-position: center center;
background-size: cover;
cursor: pointer;
width: 60%;
height: 60%;
/* gradient */
&::before {
content: '';
display: block;
position: absolute;
top: 0;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==);
background-position: top;
background-repeat: repeat-x;
height: 60px;
padding-bottom: 50px;
width: 100%;
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
}
/* responsive iframe with a 16:9 aspect ratio
thanks https://css-tricks.com/responsive-iframes/
*/
&::after {
content: '';
display: block;
padding-bottom: calc(100% / (16 / 9));
}
&:hover > .PlayerButton {
opacity: 1;
}
& > iframe {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
/* Play button */
& > .PlayerButton {
background: none;
border: none;
background-image: url('/icons/youtube.svg');
width: 68px;
height: 68px;
opacity: 0.8;
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
}
& > .PlayerButton,
& > .PlayerButton:before {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
}
/* Post-click styles */
&.lyt-activated {
cursor: unset;
}
&.lyt-activated::before,
&.lyt-activated > .PlayerButton {
opacity: 0;
pointer-events: none;
}
}
}
}
.PartyInfo {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit;
margin: 0 auto;
margin-bottom: $unit * 2;
max-width: $unit * 94;
width: 100%;
.Left {
flex-grow: 1;
h1 { h1 {
font-size: $font-xlarge; font-size: $font-xlarge;
font-weight: $normal; font-weight: $normal;
text-align: left; text-align: left;
margin-bottom: $unit; margin-bottom: $unit;
}
.info {
align-items: center;
display: flex;
flex-direction: row;
gap: $unit;
margin-bottom: $unit * 2;
.left {
flex-grow: 1;
h1 {
color: var(--text-primary); color: var(--text-primary);
&.empty { &.empty {
color: var(--text-secondary); color: var(--text-secondary);
} }
} }
}
}
.attribution { .attribution {
align-items: center; align-items: center;
@ -117,6 +193,7 @@
margin: 0 calc($unit / 2); margin: 0 calc($unit / 2);
} }
} }
}
.user { .user {
align-items: center; align-items: center;
@ -147,13 +224,3 @@
} }
} }
} }
.EmptyDetails {
display: none;
justify-content: center;
margin: $unit-4x 0 $unit-10x;
&.Visible {
display: flex;
}
}

View file

@ -1,11 +1,13 @@
import React, { useState } from 'react' import React, { useEffect, useState } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import Linkify from 'react-linkify' import Linkify from 'react-linkify'
import LiteYouTubeEmbed from 'react-lite-youtube-embed'
import classNames from 'classnames' import classNames from 'classnames'
import reactStringReplace from 'react-string-replace'
import * as AlertDialog from '@radix-ui/react-alert-dialog' import * as AlertDialog from '@radix-ui/react-alert-dialog'
@ -23,6 +25,7 @@ import CrossIcon from '~public/icons/Cross.svg'
import EditIcon from '~public/icons/Edit.svg' import EditIcon from '~public/icons/Edit.svg'
import './index.scss' import './index.scss'
import { youtube } from '~utils/youtube'
// Props // Props
interface Props { interface Props {
@ -47,11 +50,13 @@ const PartyDetails = (props: Props) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [raidSlug, setRaidSlug] = useState('') const [raidSlug, setRaidSlug] = useState('')
const [embeddedDescription, setEmbeddedDescription] =
useState<React.ReactNode>()
const readOnlyClasses = classNames({ const readOnlyClasses = classNames({
PartyDetails: true, PartyDetails: true,
ReadOnly: true, ReadOnly: true,
Visible: true, Visible: !open,
}) })
const editableClasses = classNames({ const editableClasses = classNames({
@ -102,6 +107,45 @@ const PartyDetails = (props: Props) => {
setErrors(newErrors) setErrors(newErrors)
} }
useEffect(() => {
// Extract the video IDs from the description
if (party.description) {
const videoIds = extractYoutubeVideoIds(party.description)
// Fetch the video titles for each ID
const fetchPromises = videoIds.map(({ id }) => fetchYoutubeData(id))
// Wait for all the video titles to be fetched
Promise.all(fetchPromises).then((videoTitles) => {
// YouTube regex
const youtubeUrlRegex =
/https:\/\/www\.youtube\.com\/watch\?v=([\w-]+)/g
// Replace the video URLs in the description with LiteYoutubeEmbed elements
const newDescription = reactStringReplace(
party.description,
youtubeUrlRegex,
(match, i) => (
<LiteYouTubeEmbed
id={match}
title={videoTitles[i]}
wrapperClass="YoutubeWrapper"
playerClass="PlayerButton"
/>
)
)
// Update the state with the new description
setEmbeddedDescription(newDescription)
})
}
}, [party.description])
async function fetchYoutubeData(videoId: string) {
return await youtube
.getVideoById(videoId, { maxResults: 1 })
.then((data) => data.items[0].snippet.localized.title)
}
function toggleDetails() { function toggleDetails() {
setOpen(!open) setOpen(!open)
} }
@ -119,6 +163,31 @@ const PartyDetails = (props: Props) => {
toggleDetails() toggleDetails()
} }
function extractYoutubeVideoIds(text: string) {
// Create a regular expression to match Youtube URLs in the text
const youtubeUrlRegex = /https:\/\/www\.youtube\.com\/watch\?v=([\w-]+)/g
// Initialize an array to store the video IDs
const videoIds = []
// Use the regular expression to find all the Youtube URLs in the text
let match
while ((match = youtubeUrlRegex.exec(text)) !== null) {
// Extract the video ID from the URL
const videoId = match[1]
// Add the video ID to the array, along with the character position of the URL
videoIds.push({
id: videoId,
url: match[0],
position: match.index,
})
}
// Return the array of video IDs
return videoIds
}
const userImage = (picture?: string, element?: string) => { const userImage = (picture?: string, element?: string) => {
if (picture && element) if (picture && element)
return ( return (
@ -268,9 +337,13 @@ const PartyDetails = (props: Props) => {
) )
const readOnly = ( const readOnly = (
<section className={readOnlyClasses}> <section className={readOnlyClasses}>{embeddedDescription}</section>
<div className="info"> )
<div className="left">
return (
<section className="DetailsWrapper">
<div className="PartyInfo">
<div className="Left">
<h1 className={!party.name ? 'empty' : ''}> <h1 className={!party.name ? 'empty' : ''}>
{party.name ? party.name : 'Untitled'} {party.name ? party.name : 'Untitled'}
</h1> </h1>
@ -289,7 +362,7 @@ const PartyDetails = (props: Props) => {
)} )}
</div> </div>
</div> </div>
<div className="right"> <div className="Right">
{party.editable ? ( {party.editable ? (
<Button <Button
accessoryIcon={<EditIcon />} accessoryIcon={<EditIcon />}
@ -301,21 +374,9 @@ const PartyDetails = (props: Props) => {
)} )}
</div> </div>
</div> </div>
{party.description ? (
<p>
<Linkify>{party.description}</Linkify>
</p>
) : (
''
)}
</section>
)
return (
<React.Fragment>
{readOnly} {readOnly}
{editable} {editable}
</React.Fragment> </section>
) )
} }