commit
3a94049595
6 changed files with 315 additions and 126 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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
43
package-lock.json
generated
43
package-lock.json
generated
|
|
@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
11
public/icons/youtube.svg
Normal file
11
public/icons/youtube.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1384_2987)">
|
||||
<path d="M66.52 17.74C65.74 14.81 64.03 12.33 61.1 11.55C55.79 10.13 34 10 34 10C34 10 12.21 10.13 6.9 11.55C3.97 12.33 2.27 14.81 1.48 17.74C0.0600001 23.05 0 34 0 34C0 34 0.0600001 44.95 1.48 50.26C2.26 53.19 3.97 55.67 6.9 56.45C12.21 57.87 34 58 34 58C34 58 55.79 57.87 61.1 56.45C64.03 55.67 65.74 53.19 66.52 50.26C67.94 44.95 68 34 68 34C68 34 67.94 23.05 66.52 17.74Z" fill="#FF0000"/>
|
||||
<path d="M45 34L27 24V44" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1384_2987">
|
||||
<rect width="68" height="68" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 684 B |
6
utils/youtube.tsx
Normal file
6
utils/youtube.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { YoutubeAPIClient } from 'youtube-api-v3-wrapper'
|
||||
|
||||
export const youtube = new YoutubeAPIClient(
|
||||
'key',
|
||||
process.env.NEXT_PUBLIC_YOUTUBE_API_KEY
|
||||
)
|
||||
Loading…
Reference in a new issue