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} - +
) } diff --git a/package-lock.json b/package-lock.json index a457b6ed..e060ebb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,9 +33,12 @@ "react-i18next": "^11.15.5", "react-infinite-scroll-component": "^6.1.0", "react-linkify": "^1.0.0-alpha", + "react-lite-youtube-embed": "^2.3.52", "react-scroll": "^1.8.5", + "react-string-replace": "^1.1.0", "sass": "^1.49.0", - "valtio": "^1.3.0" + "valtio": "^1.3.0", + "youtube-api-v3-wrapper": "^2.3.0" }, "devDependencies": { "@types/lodash.clonedeep": "^4.5.6", @@ -6902,6 +6905,15 @@ "tlds": "^1.199.0" } }, + "node_modules/react-lite-youtube-embed": { + "version": "2.3.52", + "resolved": "https://registry.npmjs.org/react-lite-youtube-embed/-/react-lite-youtube-embed-2.3.52.tgz", + "integrity": "sha512-G010PvCavA4EqL8mZ/Sv9XXiHnjMfONW+lmNeCRnSEPluPdptv2lZ0cNlngrj7K9j7luc8pbpyrmNpKbD9VMmw==", + "peerDependencies": { + "react": ">=16.0.8", + "react-dom": ">=16.0.8" + } + }, "node_modules/react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -6978,6 +6990,14 @@ "react-dom": "^15.5.4 || ^16.0.0 || ^17.0.0" } }, + "node_modules/react-string-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.0.tgz", + "integrity": "sha512-N6RalSDFGbOHs0IJi1H611WbZsvk3ZT47Jl2JEXFbiS3kTwsdCYij70Keo/tWtLy7sfhDsYm7CwNM/WmjXIaMw==", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -7889,6 +7909,11 @@ "engines": { "node": ">= 6" } + }, + "node_modules/youtube-api-v3-wrapper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/youtube-api-v3-wrapper/-/youtube-api-v3-wrapper-2.3.0.tgz", + "integrity": "sha512-/c4B0BSe2BEElGHO/VJt4KCqrScl7R7xG44BugvuGsBciP+fF03JN7gS/X0jnaGHOnng7GP1n320hDjwquZOgA==" } }, "dependencies": { @@ -12843,6 +12868,12 @@ "tlds": "^1.199.0" } }, + "react-lite-youtube-embed": { + "version": "2.3.52", + "resolved": "https://registry.npmjs.org/react-lite-youtube-embed/-/react-lite-youtube-embed-2.3.52.tgz", + "integrity": "sha512-G010PvCavA4EqL8mZ/Sv9XXiHnjMfONW+lmNeCRnSEPluPdptv2lZ0cNlngrj7K9j7luc8pbpyrmNpKbD9VMmw==", + "requires": {} + }, "react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -12892,6 +12923,11 @@ "prop-types": "^15.7.2" } }, + "react-string-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.0.tgz", + "integrity": "sha512-N6RalSDFGbOHs0IJi1H611WbZsvk3ZT47Jl2JEXFbiS3kTwsdCYij70Keo/tWtLy7sfhDsYm7CwNM/WmjXIaMw==" + }, "react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -13524,6 +13560,11 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, + "youtube-api-v3-wrapper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/youtube-api-v3-wrapper/-/youtube-api-v3-wrapper-2.3.0.tgz", + "integrity": "sha512-/c4B0BSe2BEElGHO/VJt4KCqrScl7R7xG44BugvuGsBciP+fF03JN7gS/X0jnaGHOnng7GP1n320hDjwquZOgA==" } } } diff --git a/package.json b/package.json index 67ce3346..6b169f96 100644 --- a/package.json +++ b/package.json @@ -38,9 +38,12 @@ "react-i18next": "^11.15.5", "react-infinite-scroll-component": "^6.1.0", "react-linkify": "^1.0.0-alpha", + "react-lite-youtube-embed": "^2.3.52", "react-scroll": "^1.8.5", + "react-string-replace": "^1.1.0", "sass": "^1.49.0", - "valtio": "^1.3.0" + "valtio": "^1.3.0", + "youtube-api-v3-wrapper": "^2.3.0" }, "devDependencies": { "@types/lodash.clonedeep": "^4.5.6", diff --git a/public/icons/youtube.svg b/public/icons/youtube.svg new file mode 100644 index 00000000..5937a0f0 --- /dev/null +++ b/public/icons/youtube.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/utils/youtube.tsx b/utils/youtube.tsx new file mode 100644 index 00000000..b3fdff5f --- /dev/null +++ b/utils/youtube.tsx @@ -0,0 +1,6 @@ +import { YoutubeAPIClient } from 'youtube-api-v3-wrapper' + +export const youtube = new YoutubeAPIClient( + 'key', + process.env.NEXT_PUBLIC_YOUTUBE_API_KEY +)