Implement Youtube embeds
This commit is contained in:
parent
5bdac9624d
commit
797215eeff
2 changed files with 252 additions and 124 deletions
|
|
@ -1,120 +1,197 @@
|
|||
.PartyDetails {
|
||||
display: none; // This breaks transition, find a workaround
|
||||
opacity: 0;
|
||||
margin: $unit-4x auto 0;
|
||||
max-width: $unit * 94;
|
||||
position: relative;
|
||||
.DetailsWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.Editable {
|
||||
top: $unit;
|
||||
height: 0;
|
||||
z-index: 2;
|
||||
transition: opacity 0.2s ease-in-out, top 0.2s ease-in-out;
|
||||
.PartyDetails {
|
||||
display: none;
|
||||
margin: 0 auto;
|
||||
max-width: $unit * 94;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
&.Visible {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
margin-bottom: $unit-12x;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: block;
|
||||
width: 100%;
|
||||
&.Editable {
|
||||
gap: $unit;
|
||||
|
||||
textarea {
|
||||
min-height: $unit * 20;
|
||||
&.Visible {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
textarea {
|
||||
min-height: $unit * 20;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.SelectTrigger {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
margin-bottom: $unit-12x;
|
||||
|
||||
.left {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.right {
|
||||
.bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.ReadOnly {
|
||||
top: $unit * -1;
|
||||
transition: opacity 0.2s ease-in-out, top 0.2s ease-in-out;
|
||||
.left {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.Visible {
|
||||
display: block;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $font-regular;
|
||||
line-height: $font-regular * 1.2;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: $font-xlarge;
|
||||
font-weight: $normal;
|
||||
text-align: left;
|
||||
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);
|
||||
|
||||
&.empty {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attribution {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
&.ReadOnly {
|
||||
&.Visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& > div {
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $font-regular;
|
||||
line-height: $font-regular * 1.2;
|
||||
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 {
|
||||
font-size: $font-xlarge;
|
||||
font-weight: $normal;
|
||||
text-align: left;
|
||||
margin-bottom: $unit;
|
||||
color: var(--text-primary);
|
||||
|
||||
&.empty {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.attribution {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
font-size: $font-small;
|
||||
height: 26px;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
time {
|
||||
font-size: $font-small;
|
||||
}
|
||||
& > div {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
font-size: $font-small;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
& > *:not(:last-child):after {
|
||||
content: ' · ';
|
||||
margin: 0 calc($unit / 2);
|
||||
time {
|
||||
font-size: $font-small;
|
||||
}
|
||||
|
||||
& > *:not(:last-child):after {
|
||||
content: ' · ';
|
||||
margin: 0 calc($unit / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,13 +224,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.EmptyDetails {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
margin: $unit-4x 0 $unit-10x;
|
||||
|
||||
&.Visible {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import Linkify from 'react-linkify'
|
||||
import LiteYouTubeEmbed from 'react-lite-youtube-embed'
|
||||
import classNames from 'classnames'
|
||||
import reactStringReplace from 'react-string-replace'
|
||||
|
||||
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 './index.scss'
|
||||
import { youtube } from '~utils/youtube'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
|
|
@ -47,11 +50,13 @@ const PartyDetails = (props: Props) => {
|
|||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [raidSlug, setRaidSlug] = useState('')
|
||||
const [embeddedDescription, setEmbeddedDescription] =
|
||||
useState<React.ReactNode>()
|
||||
|
||||
const readOnlyClasses = classNames({
|
||||
PartyDetails: true,
|
||||
ReadOnly: true,
|
||||
Visible: true,
|
||||
Visible: !open,
|
||||
})
|
||||
|
||||
const editableClasses = classNames({
|
||||
|
|
@ -102,6 +107,45 @@ const PartyDetails = (props: Props) => {
|
|||
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() {
|
||||
setOpen(!open)
|
||||
}
|
||||
|
|
@ -119,6 +163,31 @@ const PartyDetails = (props: Props) => {
|
|||
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) => {
|
||||
if (picture && element)
|
||||
return (
|
||||
|
|
@ -268,9 +337,13 @@ const PartyDetails = (props: Props) => {
|
|||
)
|
||||
|
||||
const readOnly = (
|
||||
<section className={readOnlyClasses}>
|
||||
<div className="info">
|
||||
<div className="left">
|
||||
<section className={readOnlyClasses}>{embeddedDescription}</section>
|
||||
)
|
||||
|
||||
return (
|
||||
<section className="DetailsWrapper">
|
||||
<div className="PartyInfo">
|
||||
<div className="Left">
|
||||
<h1 className={!party.name ? 'empty' : ''}>
|
||||
{party.name ? party.name : 'Untitled'}
|
||||
</h1>
|
||||
|
|
@ -289,7 +362,7 @@ const PartyDetails = (props: Props) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="right">
|
||||
<div className="Right">
|
||||
{party.editable ? (
|
||||
<Button
|
||||
accessoryIcon={<EditIcon />}
|
||||
|
|
@ -301,21 +374,9 @@ const PartyDetails = (props: Props) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{party.description ? (
|
||||
<p>
|
||||
<Linkify>{party.description}</Linkify>
|
||||
</p>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{readOnly}
|
||||
{editable}
|
||||
</React.Fragment>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue