diff --git a/components/PartyDetails/index.scss b/components/PartyDetails/index.scss index f5297a37..f4213504 100644 --- a/components/PartyDetails/index.scss +++ b/components/PartyDetails/index.scss @@ -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; - } -} diff --git a/components/PartyDetails/index.tsx b/components/PartyDetails/index.tsx index a2946078..eb59c2cb 100644 --- a/components/PartyDetails/index.tsx +++ b/components/PartyDetails/index.tsx @@ -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() 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) => ( + + ) + ) + + // 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 = ( -
-
-
+
{embeddedDescription}
+ ) + + return ( +
+
+

{party.name ? party.name : 'Untitled'}

@@ -289,7 +362,7 @@ const PartyDetails = (props: Props) => { )}
-
+
{party.editable ? (
- {party.description ? ( -

- {party.description} -

- ) : ( - '' - )} -
- ) - - return ( - {readOnly} {editable} - +
) }