diff --git a/package.json b/package.json index 6e54a5a..3abd415 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "react-blurhash": "^0.3.0", "react-dom": "18.2.0", "react-imgix": "^9.7.0", + "react-player": "^2.12.0", "typescript": "5.0.4" }, "devDependencies": { diff --git a/src/components/Article/Article.Block.tsx b/src/components/Article/Article.Block.tsx index a9914ad..b27fdce 100644 --- a/src/components/Article/Article.Block.tsx +++ b/src/components/Article/Article.Block.tsx @@ -4,9 +4,11 @@ import { extractIdFromFirstTag, extractInnerHtml, } from '@/utils/html.utils' +import { convertToIframe } from '@/utils/string.utils' import { HeadingElementsRef } from '@/utils/ui.utils' import { Typography } from '@acid-info/lsd-react' import styled from '@emotion/styled' +import ReactPlayer from 'react-player' import { LPE } from '../../types/lpe.types' import { PostImageRatio } from '../Post/Post' import { ArticleImageBlockWrapper } from './Article.ImageBlockWrapper' @@ -43,6 +45,55 @@ export const RenderArticleBlock = ({ /> ) } + case 'p': { + const isIframeRegex = /]*>(?:<\/iframe>|[^]*?<\/iframe>)/ + const isIframe = isIframeRegex.test(block.text) + + const isYoutubeRegex = + /^(https?\:\/\/)?((www\.)?youtube\.com|youtu\.?be)\/.+$/ + + const isYoutube = isYoutubeRegex.test(block.text) + const youtubeLink = block.text.match(isYoutubeRegex) ?? [] + + const isSimplecastRegex = + /^https?:\/\/([a-zA-Z0-9-]+\.)*simplecast\.com\/[^?\s]+(\?[\s\S]*)?$/ + + const isSimplecast = isSimplecastRegex.test(block.text) + const simplecastLink = block.text.match(isSimplecastRegex) ?? [] + + // const episodeId = extractUUIDFromEpisode(simplecastLink[0] ?? '') + + // let audioSrc = '' + + // if (isSimplecast) { + // fetch( + // `https://api.simplecast.com/episodes/audio/bc313c16-82e9-439a-8e0c-af59833d22d7`, + // ) + // .then((response) => response.json()) + // .then((data) => console.log(data)) + // } + + return isIframe ? ( + + ) : isYoutube ? ( + + ) : isSimplecast ? ( + + ) : ( + + ) + } default: return ( iframe, + & > object, + & > embed { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } +` diff --git a/src/components/GlobalAudioPlayer/GlobalAudioPlayer.module.css b/src/components/GlobalAudioPlayer/GlobalAudioPlayer.module.css new file mode 100644 index 0000000..fb6fdbb --- /dev/null +++ b/src/components/GlobalAudioPlayer/GlobalAudioPlayer.module.css @@ -0,0 +1,20 @@ +.audioPlayer > input[type='range'] { + width: 100%; + accent-color: black; +} + +.audioPlayer > input[type='range']:focus { + outline: none; +} + +.audioPlayer > input[type='range']::-webkit-slider-runnable-track { + width: 100%; + height: 8px; + cursor: grabbing; +} + +.audioPlayer > input[type='range']::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + margin-top: -4px; +} \ No newline at end of file diff --git a/src/components/GlobalAudioPlayer/GlobalAudioPlayer.tsx b/src/components/GlobalAudioPlayer/GlobalAudioPlayer.tsx new file mode 100644 index 0000000..b878777 --- /dev/null +++ b/src/components/GlobalAudioPlayer/GlobalAudioPlayer.tsx @@ -0,0 +1,273 @@ +import ReactPlayer from 'react-player' +import styled from '@emotion/styled' +import { useRef, useState } from 'react' +import { PlayIcon } from '../Icons/PlayIcon' +import { PauseIcon } from '../Icons/PauseIcon' +import { VolumeIcon } from '../Icons/VolumeIcon' +import styles from './GlobalAudioPlayer.module.css' +import { convertSecToMinAndSec } from '@/utils/string.utils' +import { Typography } from '@acid-info/lsd-react' + +type StateProps = { + url: string | null + pip: boolean + playing: boolean + playedSeconds: number + controls: boolean + light: boolean + volume: number + muted: boolean + played: number + loaded: number + duration: number + playbackRate: number + loop: boolean + seeking: boolean +} + +const TEMP_URL = + 'https://cdn.simplecast.com/audio/b54c0885-7c72-415d-b032-7d294b78d785/episodes/30d4e2f5-4434-419c-8fc1-a76e4b367e20/audio/3c8eb229-3f34-45a4-84f1-ce1d6bd65922/default_tc.mp3' + +export default function GlobalAudioPlayer() { + const ref = useRef(null) + const [state, setState] = useState({ + url: TEMP_URL, + pip: false, + playing: false, + playedSeconds: 0, + controls: false, + light: false, + volume: 0.8, + muted: false, + played: 0, + loaded: 0, + duration: 0, + playbackRate: 1.0, + loop: false, + seeking: false, + }) + const [showVolume, setShowVolume] = useState(false) + + // const handleLoad = (url: string) => { + // setState((prev) => ({ ...prev, url, played: 0, loaded: 0, pip: false })) + // } + + const handlePlay = () => { + setState((prev) => ({ ...prev, playing: true })) + } + + const handlePlayPause = () => { + setState((prev) => ({ ...prev, playing: !state.playing })) + } + + // const handleStop = () => { + // setState((prev) => ({ ...prev, url: null, playing: false })) + // } + + const handleEnded = () => { + setState((prev) => ({ ...prev, playing: prev.loop })) + } + + const handleVolumeChange = (e: React.ChangeEvent) => { + setState((prev) => ({ ...prev, volume: parseFloat(e.target.value) })) + } + + // const handleToggleMuted = (e: React.ChangeEvent) => { + // setState((prev) => ({ ...prev, muted: !state.muted })) + // } + + // const handleSetPlaybackRate = (e: React.ChangeEvent) => { + // setState((prev) => ({ ...prev, playbackRate: parseFloat(e.target.value) })) + // } + + const handlePause = () => { + setState((prev) => ({ ...prev, playing: false })) + } + + const handleSeekMouseDown = ( + e: React.MouseEvent, + ) => { + setState((prev) => ({ ...prev, seeking: true })) + } + + const handleSeekChange = (e: React.ChangeEvent) => { + setState((prev) => ({ ...prev, played: parseFloat(e.target.value) })) + } + + const handleSeekMouseUp = ( + e: React.MouseEvent, + ) => { + setState((prev) => ({ ...prev, seeking: false })) + const target = e.target as HTMLInputElement + ref.current?.seekTo(parseFloat(target?.value)) + } + + const handleDuration = (duration: number) => { + setState((prev) => ({ ...prev, duration })) + } + + const handleOnPlaybackRateChange = ( + e: React.ChangeEvent, + ) => { + setState((prev) => ({ ...prev, playbackRate: parseFloat(e.target.value) })) + } + + const handleProgress = (newState: { playedSeconds: number }) => { + setState((prev) => ({ ...prev, playedSeconds: newState.playedSeconds })) + } + + return ( + + + + + + {state.playing ? : } + + + + / + + + + + setShowVolume((prev) => !prev)}> + {showVolume && ( + + + + )} + + + + + {/* */} + + + + + + + console.log('onReady')} + onStart={() => console.log('onStart')} + onPlay={handlePlay} + onPause={handlePause} + onBuffer={() => console.log('onBuffer')} + onPlaybackRateChange={handleOnPlaybackRateChange} + onSeek={(e) => console.log('onSeek', e)} + onEnded={handleEnded} + onError={(e) => console.log('onError', e)} + onDuration={handleDuration} + onProgress={handleProgress} + /> + + + ) +} + +const Container = styled.div` + width: 100vw; + height: 80px; + padding: 22px 16px; + background: rgb(var(--lsd-surface-primary)); + position: fixed; + display: flex; + align-items: center; + justify-content: center; + bottom: 0; + left: 0; + border-top: 1px solid rgb(var(--lsd-border-primary)); + box-sizing: border-box; +` + +const Buttons = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +` + +const VolumeContainer = styled.div` + display: flex; + justify-content: center; + position: relative; + align-items: center; +` + +const Seek = styled.div` + display: flex; + width: 100%; +` + +const VolumeGauge = styled.div` + position: absolute; + top: -30px; +` + +const AudioPlayer = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + width: 60%; +` + +const RightMenu = styled.div` + width: 40%; +` + +const PlayPause = styled.button` + display: flex; + align-items: center; + justify-content: center; + border: none; + background: none; + margin-right: 8px; +` + +const Row = styled.div` + display: flex; + align-items: center; + white-space: pre-wrap; +` + +const TimeContainer = styled(Row)` + gap: 8px; +` + +const Time = styled(Typography)` + width: 32px; +` diff --git a/src/components/GlobalAudioPlayer/index.ts b/src/components/GlobalAudioPlayer/index.ts new file mode 100644 index 0000000..97edc94 --- /dev/null +++ b/src/components/GlobalAudioPlayer/index.ts @@ -0,0 +1 @@ +export { default as GlobalAudioPlayer } from './GlobalAudioPlayer' diff --git a/src/components/Icons/PauseIcon/PauseIcon.tsx b/src/components/Icons/PauseIcon/PauseIcon.tsx new file mode 100644 index 0000000..fd853c9 --- /dev/null +++ b/src/components/Icons/PauseIcon/PauseIcon.tsx @@ -0,0 +1,22 @@ +import { LsdIcon } from '@acid-info/lsd-react' + +export const PauseIcon = LsdIcon( + (props) => ( + + + + + + + ), + { filled: true }, +) diff --git a/src/components/Icons/PauseIcon/index.ts b/src/components/Icons/PauseIcon/index.ts new file mode 100644 index 0000000..6802941 --- /dev/null +++ b/src/components/Icons/PauseIcon/index.ts @@ -0,0 +1 @@ +export * from './PauseIcon' diff --git a/src/components/Icons/PlayIcon/PlayIcon.tsx b/src/components/Icons/PlayIcon/PlayIcon.tsx new file mode 100644 index 0000000..5ba8b5c --- /dev/null +++ b/src/components/Icons/PlayIcon/PlayIcon.tsx @@ -0,0 +1,20 @@ +import { LsdIcon } from '@acid-info/lsd-react' + +export const PlayIcon = LsdIcon( + (props) => ( + + + + ), + { filled: true }, +) diff --git a/src/components/Icons/PlayIcon/index.ts b/src/components/Icons/PlayIcon/index.ts new file mode 100644 index 0000000..63c61c0 --- /dev/null +++ b/src/components/Icons/PlayIcon/index.ts @@ -0,0 +1 @@ +export * from './PlayIcon' diff --git a/src/components/Icons/VolumeIcon/VolumeIcon.tsx b/src/components/Icons/VolumeIcon/VolumeIcon.tsx new file mode 100644 index 0000000..ff375a5 --- /dev/null +++ b/src/components/Icons/VolumeIcon/VolumeIcon.tsx @@ -0,0 +1,27 @@ +import { LsdIcon } from '@acid-info/lsd-react' + +export const VolumeIcon = LsdIcon( + (props) => ( + + + + + + + + + + + ), + { filled: true }, +) diff --git a/src/components/Icons/VolumeIcon/index.ts b/src/components/Icons/VolumeIcon/index.ts new file mode 100644 index 0000000..78b4732 --- /dev/null +++ b/src/components/Icons/VolumeIcon/index.ts @@ -0,0 +1 @@ +export * from './VolumeIcon' diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 4285c00..90339ef 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -8,6 +8,7 @@ import type { AppProps } from 'next/app' import Head from 'next/head' import { ReactNode } from 'react' import { LSDThemeProvider } from '../containers/LSDThemeProvider' +import { GlobalAudioPlayer } from '@/components/GlobalAudioPlayer' type NextLayoutComponentType

= NextComponentType< NextPageContext, @@ -94,6 +95,7 @@ export default function App({ Component, pageProps }: AppLayoutProps) { {getLayout()} + ) } diff --git a/src/styles/globals.css b/src/styles/globals.css index e69de29..8b13789 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -0,0 +1 @@ + diff --git a/src/utils/data.utils.ts b/src/utils/data.utils.ts index 6678cdc..c122c0d 100644 --- a/src/utils/data.utils.ts +++ b/src/utils/data.utils.ts @@ -24,3 +24,25 @@ export const shuffle = (array: any[]) => { } export const unique = (arr: any[]) => Array.from(new Set(arr)) + +export const getAudioSourceFromSimplecastPlayer = async (url: string) => { + const myHeaders = new Headers() + myHeaders.append( + 'Authorization', + 'Bearer eyJhcGlfa2V5IjoiMzg3OTdhY2Y5N2NmZjgzZjQxNGI5ODNiN2E2MjY3NmQifQ==', + ) + + const requestOptions = { + method: 'GET', + headers: myHeaders, + } + + const result = await fetch( + `https://api.simplecast.com/episodes/${url}`, + requestOptions, + ) + + const data = await result.json() + console.log(data) + return data +} diff --git a/src/utils/string.utils.ts b/src/utils/string.utils.ts index 44e963b..fcd3f28 100644 --- a/src/utils/string.utils.ts +++ b/src/utils/string.utils.ts @@ -41,3 +41,27 @@ export const calcReadingTime = (text: string): number => { const numberOfWords = text.split(/\s/g).length return Math.ceil(numberOfWords / wordsPerMinute) } + +export function convertSecToMinAndSec(totalSeconds: number) { + // Convert seconds to minutes and seconds + const minutes = Math.floor(totalSeconds / 60) + const seconds = Math.floor(totalSeconds % 60) + + // Ensure two digit format + const formattedMinutes = String(minutes).padStart(2, '0') + const formattedSeconds = String(seconds).padStart(2, '0') + + return `${formattedMinutes}:${formattedSeconds}` +} + +export function extractUUIDFromEpisode(url: string) { + const regex = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/ + const match = url.match(regex) + return match ? match[1] : null +} + +export function convertToIframe(url: string) { + if (!url) return '' + + return `` +} diff --git a/src/utils/ui.utils.ts b/src/utils/ui.utils.ts index 6525d9d..2b45f1c 100644 --- a/src/utils/ui.utils.ts +++ b/src/utils/ui.utils.ts @@ -83,7 +83,6 @@ export function useIntersectionObserver( headings.forEach((heading) => { if (heading.isIntersecting && heading.target instanceof HTMLElement) { const targetId = heading.target.getAttribute('id') - console.log(targetId) if (targetId) setActiveId(targetId) } }) diff --git a/yarn.lock b/yarn.lock index b2e0c18..87e3827 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2438,6 +2438,11 @@ defaults@^1.0.3: integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== dependencies: clone "^1.0.2" + +deepmerge@^4.0.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== define-lazy-prop@^2.0.0: version "2.0.0" @@ -3938,6 +3943,11 @@ locate-path@^5.0.0: integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== dependencies: p-locate "^4.1.0" + +load-script@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4" + integrity sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA== locate-path@^6.0.0: version "6.0.0" @@ -4033,6 +4043,11 @@ mdn-data@2.0.14: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== +memoize-one@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -4616,6 +4631,11 @@ react-dom@18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-fast-compare@^3.0.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" + integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== + react-imgix@^9.7.0: version "9.7.0" resolved "https://registry.yarnpkg.com/react-imgix/-/react-imgix-9.7.0.tgz#944f63693daf6524d07898aaf7d1cbbe59e5edca" @@ -4642,6 +4662,17 @@ react-measure@^2.3.0: prop-types "^15.6.2" resize-observer-polyfill "^1.5.0" +react-player@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/react-player/-/react-player-2.12.0.tgz#2fc05dbfec234c829292fbca563b544064bd14f0" + integrity sha512-rymLRz/2GJJD+Wc01S7S+i9pGMFYnNmQibR2gVE3KmHJCBNN8BhPAlOPTGZtn1uKpJ6p4RPLlzPQ1OLreXd8gw== + dependencies: + deepmerge "^4.0.0" + load-script "^1.0.0" + memoize-one "^5.1.1" + prop-types "^15.7.2" + react-fast-compare "^3.0.1" + react-universal-interface@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b"