Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions components/Nft/NftContentRenderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { useTranslation } from 'next-i18next'
import Head from 'next/head'
import { useRef, useEffect } from 'react'
import ReactPannellum from 'react-pannellum'
import LoadingGif from '../../public/images/loading.gif'
import {
needNftAgeCheck,
nftName,
renderLoadingImage,
handle18PlusClick
} from '../../utils/nft'

export default function NftContentRenderer({
nft,
contentTab,
imageUrl,
videoUrl,
modelUrl,
defaultTab,
defaultUrl,
clUrl,
imageStyle,
modelAttr,
isPanoramic,
loaded,
errored,
setLoaded,
setErrored,
setShowAgeCheck,
classNamePrefix = 'fv-preview',
panoramaIds = { image: '1', video: '2' },
panoramaSceneIds = { image: 'firstScene', video: 'videoScene' }
}) {
const { t } = useTranslation()
const modelViewerRef = useRef(null)

const clickOn18PlusImage = () => handle18PlusClick(setShowAgeCheck)

useEffect(() => {
const modelViewer = modelViewerRef.current
if (!modelViewer || defaultTab !== 'model') return

const handleLoad = () => {
setLoaded(true)
setErrored(false)
}

const handleError = () => {
setErrored(true)
}

modelViewer.addEventListener('load', handleLoad)
modelViewer.addEventListener('error', handleError)

return () => {
modelViewer.removeEventListener('load', handleLoad)
modelViewer.removeEventListener('error', handleError)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultTab, modelUrl])

return (
<>
{needNftAgeCheck(nft) ? (
<img
src="/images/nft/18plus.jpg"
className={`${classNamePrefix}-age-image`}
onClick={clickOn18PlusImage}
alt="18 plus content"
/>
) : (
<>
{imageUrl && contentTab === 'image' && (
<>
{renderLoadingImage(errored, loaded, t)}
{isPanoramic ? (
<ReactPannellum
id={panoramaIds.image}
sceneId={panoramaSceneIds.image}
imageSource={defaultUrl}
config={{
autoLoad: true,
autoRotate: -2
}}
className={`${classNamePrefix}-panorama`}
style={{ display: loaded ? 'inline-block' : 'none' }}
/>
) : (
<img
className={classNamePrefix === 'fv-fullscreen' ? 'fv-fullscreen-media-image' : `${classNamePrefix}-image`}
style={(() => {
if (classNamePrefix === 'fv-fullscreen') {
return {
imageRendering: imageStyle.imageRendering,
filter: imageStyle.filter,
display: loaded ? 'inline-block' : 'none'
}
}
return {
...imageStyle,
display: loaded ? 'inline-block' : 'none'
}
})()}
Comment on lines +91 to +103
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline IIFE for computing styles is complex and reduces readability. Extract this logic into a named function or compute the style object before the return statement to improve code clarity.

Copilot uses AI. Check for mistakes.
src={imageUrl}
onLoad={() => {
setLoaded(true)
setErrored(false)
}}
onError={({ currentTarget }) => {
if (currentTarget.src === imageUrl && imageUrl !== clUrl.image) {
currentTarget.src = clUrl.image
} else {
setErrored(true)
}
}}
alt={nftName(nft)}
/>
)}
</>
)}

{videoUrl && defaultTab === 'video' && (
<>
{renderLoadingImage(errored, loaded, t)}
{isPanoramic ? (
<ReactPannellum
id={panoramaIds.video}
sceneId={panoramaSceneIds.video}
imageSource={videoUrl}
config={{
autoLoad: true,
autoRotate: 0
}}
className={`${classNamePrefix}-panorama`}
style={{ display: loaded ? 'inline-block' : 'none' }}
/>
) : (
<video
autoPlay
playsInline
muted
loop
controls
className={`${classNamePrefix}-video`}
style={{ display: loaded ? 'block' : 'none' }}
onLoadedData={() => {
setLoaded(true)
setErrored(false)
}}
onError={() => {
setErrored(true)
}}
>
<source src={videoUrl} type="video/mp4" />
</video>
)}
</>
)}

{modelUrl && defaultTab === 'model' && (
<>
{renderLoadingImage(errored, loaded, t)}
<Head>
<script type="module" src="/js/model-viewer.min.js?v=2" defer />
</Head>
<model-viewer
ref={modelViewerRef}
className={`model-viewer ${classNamePrefix === 'fv-fullscreen' ? 'fv-fullscreen-model' : ''}`}
src={modelUrl}
camera-controls
auto-rotate
ar
poster={LoadingGif}
autoplay
ar-modes="webxr scene-viewer quick-look"
style={{ opacity: loaded ? 1 : 0 }}
{...modelAttr?.reduce((prev, curr) => {
prev[curr.attribute] = curr.value
return prev
}, {})}
></model-viewer>
</>
)}
</>
)}
</>
)
}

165 changes: 165 additions & 0 deletions components/NftFullScreenViewer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { useEffect } from 'react'
import Head from 'next/head'

import {
nftName,
renderDownloadButton
} from '../utils/nft'

import { useNftContent } from '../hooks/useNftContent'
import { FaTimes } from 'react-icons/fa'
import AgeCheck from './UI/AgeCheck'
import NftContentRenderer from './Nft/NftContentRenderer'
import styles from '../styles/components/nftFullScreenViewer.module.scss'

export default function NftFullScreenViewer({ nft, onClose }) {
const {
contentTab,
setContentTab,
loaded,
setLoaded,
errored,
setErrored,
isPanoramic,
showAgeCheck,
setShowAgeCheck,
imageUrl,
videoUrl,
audioUrl,
modelUrl,
clUrl,
contentTabList,
imageStyle,
defaultTab,
defaultUrl,
modelAttr,
t
} = useNftContent(nft, { baseStyle: { width: '100%', height: 'auto', maxHeight: '76vh' } })

useEffect(() => {
document.body.style.overflow = 'hidden'

document.body.setAttribute('data-fullscreen-open', 'true')

return () => {
document.body.style.overflow = 'unset'
document.body.setAttribute('data-fullscreen-open', 'false')
}
}, [])


// Reusable button rendering functions
const renderTabButtons = (containerClass = '') => (
contentTabList.length > 1 && (
<div className={`fv-fullscreen-tabs-container ${containerClass}`}>
{contentTabList.map(tab => (
<button
key={tab.value}
onClick={() => setContentTab(tab.value)}
className={`fv-fullscreen-tab-button ${contentTab === tab.value ? 'fv-fullscreen-tab-button-active' : ''}`}
>
{tab.label}
</button>
))}
</div>
)
);

const renderActionButtons = (containerClass = '') => {
// Use the actual content URL for download, not the clUrl
let downloadUrl = null
if (contentTab === 'image' && imageUrl) {
downloadUrl = imageUrl
} else if (contentTab === 'video' && videoUrl) {
downloadUrl = videoUrl
} else if (contentTab === 'model' && modelUrl) {
downloadUrl = modelUrl
} else if (contentTab === 'audio' && audioUrl) {
downloadUrl = audioUrl
}

return (
<div className={`fv-fullscreen-actions-container ${containerClass}`}>
{downloadUrl && renderDownloadButton(downloadUrl, t('tabs.' + contentTab), 'fv-fullscreen-button fv-fullscreen-action-button')}

<button
onClick={onClose}
className="fv-fullscreen-button fv-fullscreen-close-button"
>
<FaTimes /> Close
</button>
</div>
);
};


return (
<div className={styles.nftFullScreenView}>
<div className="fv-fullscreen-root">
<Head>
<title>{nftName(nft) || 'NFT Viewer'} - Full Screen</title>
</Head>

{/* Header with close button and tabs */}
<div className="fv-fullscreen-header">
<div className="fv-fullscreen-header-left">
<h2 className="fv-fullscreen-title">
{nftName(nft) || 'NFT Viewer'}
</h2>

{renderTabButtons()}
</div>

{renderActionButtons()}

{/* Mobile buttons row */}
<div className="fv-fullscreen-mobile-buttons">
{renderTabButtons()}
{renderActionButtons()}
</div>
</div>

{/* Content Area */}
<div className="fv-fullscreen-content">
<NftContentRenderer
nft={nft}
contentTab={contentTab}
imageUrl={imageUrl}
videoUrl={videoUrl}
modelUrl={modelUrl}
defaultTab={defaultTab}
defaultUrl={defaultUrl}
clUrl={clUrl}
imageStyle={imageStyle}
modelAttr={modelAttr}
isPanoramic={isPanoramic}
loaded={loaded}
errored={errored}
setLoaded={setLoaded}
setErrored={setErrored}
setShowAgeCheck={setShowAgeCheck}
classNamePrefix="fv-fullscreen"
panoramaIds={{ image: 'fullscreen-panorama', video: 'fullscreen-video-panorama' }}
panoramaSceneIds={{ image: 'fullscreenScene', video: 'fullscreenVideoScene' }}
/>
</div>

{/* Audio player at bottom if applicable */}
{defaultTab !== 'model' && defaultTab !== 'video' && audioUrl && (
<div className="fv-fullscreen-audio-bar">
<div className="fv-fullscreen-audio-row">
<audio
src={audioUrl}
controls
className="fv-fullscreen-audio"
/>
{renderDownloadButton(audioUrl, t('tabs.audio'), 'fv-fullscreen-button fv-fullscreen-action-button')}
</div>
</div>
)}

{showAgeCheck && <AgeCheck setShowAgeCheck={setShowAgeCheck} />}
</div>
</div>
)
}
Loading