diff --git a/components/NftFullScreenViewer.js b/components/NftFullScreenViewer.js
new file mode 100644
index 000000000..75ddb9e8c
--- /dev/null
+++ b/components/NftFullScreenViewer.js
@@ -0,0 +1,449 @@
+import { useState, useEffect } from 'react'
+import { useTranslation } from 'next-i18next'
+import Head from 'next/head'
+
+import { forbid18Plus, stripText } from '../utils'
+import { needNftAgeCheck, nftName, nftUrl } from '../utils/nft'
+
+import LoadingGif from '../public/images/loading.gif'
+import { FaCloudDownloadAlt, FaTimes } from 'react-icons/fa'
+import ReactPannellum from 'react-pannellum'
+import AgeCheck from './UI/AgeCheck'
+
+const downloadIcon = (
+
+
+
+)
+
+const isPanorama = (metadata) => {
+ if (!metadata) return false
+
+ // Check name and description for panorama keywords
+ const panoramaKeywords = ['360', 'panorama', 'panoramic', 'equirectangular']
+ const name = metadata.name?.toString().toLowerCase() || ''
+ const description = metadata.description?.toString().toLowerCase() || ''
+
+ // Check if name or description contains panorama keywords
+ const hasPanoramaKeyword = panoramaKeywords.some((keyword) => name.includes(keyword) || description.includes(keyword))
+
+ // Check for specific camera types known for panoramas
+ const panoramaCameras = ['gopro fusion', 'insta360', 'ricoh theta']
+ const hasPanoramaCamera = panoramaCameras.some((camera) => description.includes(camera.toLowerCase()))
+
+ return hasPanoramaKeyword || hasPanoramaCamera
+}
+
+export default function NftFullScreenViewer({ nft, onClose }) {
+ const { t } = useTranslation()
+ const [contentTab, setContentTab] = useState('image')
+ const [loaded, setLoaded] = useState(false)
+ const [errored, setErrored] = useState(false)
+ const [isPanoramic, setIsPanoramic] = useState(false)
+ const [showAgeCheck, setShowAgeCheck] = useState(false)
+
+ const imageUrl = nftUrl(nft, 'image')
+ const videoUrl = nftUrl(nft, 'video')
+ const audioUrl = nftUrl(nft, 'audio')
+ const modelUrl = nftUrl(nft, 'model')
+
+ const clUrl = {
+ image: nftUrl(nft, 'image', 'cl'),
+ video: nftUrl(nft, 'video', 'cl'),
+ audio: nftUrl(nft, 'audio', 'cl'),
+ model: nftUrl(nft, 'model', 'cl')
+ }
+
+ const contentTabList = []
+ if (imageUrl) {
+ contentTabList.push({ value: 'image', label: t('tabs.image') })
+ }
+ if (videoUrl) {
+ contentTabList.push({ value: 'video', label: t('tabs.video') })
+ }
+ if (modelUrl) {
+ contentTabList.push({ value: 'model', label: t('tabs.model') })
+ }
+
+ let imageStyle = { width: '100%', height: 'auto', maxHeight: '80vh' }
+ if (imageUrl) {
+ if (imageUrl.slice(0, 10) === 'data:image') {
+ imageStyle.imageRendering = 'pixelated'
+ }
+ if (nft.deletedAt) {
+ imageStyle.filter = 'grayscale(1)'
+ }
+ }
+
+ let defaultTab = contentTab
+ let defaultUrl = clUrl[contentTab]
+ if (!imageUrl && contentTab === 'image') {
+ if (clUrl['video']) {
+ defaultTab = 'video'
+ defaultUrl = clUrl['video']
+ } else if (clUrl['model']) {
+ defaultTab = 'model'
+ defaultUrl = clUrl['model']
+ }
+ }
+
+ //add attributes for the 3D model viewer
+ let modelAttr = []
+ if (nft.metadata && (nft.metadata['3D_attributes'] || nft.metadata['3d_attributes'])) {
+ modelAttr = nft.metadata['3D_attributes'] || nft.metadata['3d_attributes']
+ const supportedAttr = [
+ 'environment-image',
+ 'exposure',
+ 'shadow-intensity',
+ 'shadow-softness',
+ 'camera-orbit',
+ 'camera-target',
+ 'skybox-image',
+ 'auto-rotate-delay',
+ 'rotation-per-second',
+ 'field-of-view',
+ 'max-camera-orbit',
+ 'min-camera-orbit',
+ 'max-field-of-view',
+ 'min-field-of-view',
+ 'disable-zoom',
+ 'orbit-sensitivity',
+ 'animation-name',
+ 'animation-crossfade-duration',
+ 'variant-name',
+ 'orientation',
+ 'scale'
+ ]
+ if (Array.isArray(modelAttr)) {
+ for (let i = 0; i < modelAttr.length; i++) {
+ if (supportedAttr.includes(modelAttr[i].attribute)) {
+ modelAttr[i].value = stripText(modelAttr[i].value)
+ } else {
+ delete modelAttr[i]
+ }
+ }
+ } else if (typeof modelAttr === 'object') {
+ let metaModelAttr = modelAttr
+ modelAttr = []
+ Object.keys(metaModelAttr).forEach((e) => {
+ if (supportedAttr.includes(e)) {
+ modelAttr.push({
+ attribute: e,
+ value: stripText(metaModelAttr[e])
+ })
+ }
+ })
+ }
+ }
+
+ useEffect(() => {
+ if (imageUrl || videoUrl) {
+ const panoramic = isPanorama(nft.metadata)
+ setIsPanoramic(panoramic)
+ if (panoramic) {
+ setLoaded(true)
+ }
+ // if no image, but there is a video, don't show loading spinner
+ if (!imageUrl) {
+ setLoaded(true)
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [imageUrl, videoUrl])
+
+ const clickOn18PlusImage = async () => {
+ const forbid = await forbid18Plus()
+ if (forbid) return
+ setShowAgeCheck(true)
+ }
+
+ const loadingImage = () => {
+ if (errored) {
+ return (
+
+ {t('general.load-failed')}
+
+
+ )
+ } else if (!loaded) {
+ return (
+
+
+
+ {t('general.loading')}
+
+ )
+ }
+ }
+
+ return (
+
+
+
{nftName(nft) || 'NFT Viewer'} - Full Screen
+
+
+ {/* Header with close button and tabs */}
+
+
+
+ {nftName(nft) || 'NFT Viewer'}
+
+
+ {contentTabList.length > 1 && (
+
+ {contentTabList.map(tab => (
+
+ ))}
+
+ )}
+
+
+
+
+
+ {/* Content Area */}
+
+ {needNftAgeCheck(nft) ? (
+

+ ) : (
+ <>
+ {imageUrl && contentTab === 'image' && (
+ <>
+ {loadingImage()}
+ {isPanoramic ? (
+
+ ) : (
+

{
+ setLoaded(true)
+ setErrored(false)
+ }}
+ onError={({ currentTarget }) => {
+ if (currentTarget.src === imageUrl && imageUrl !== clUrl.image) {
+ currentTarget.src = clUrl.image
+ } else {
+ setErrored(true)
+ }
+ }}
+ alt={nftName(nft)}
+ />
+ )}
+ >
+ )}
+
+ {videoUrl && contentTab === 'video' && (
+ <>
+ {loadingImage()}
+ {isPanoramic ? (
+
+ ) : (
+
+ )}
+ >
+ )}
+
+ {modelUrl && contentTab === 'model' && (
+ <>
+
+
+
+
{
+ prev[curr.attribute] = curr.value
+ return prev
+ }, {})}
+ >
+ >
+ )}
+ >
+ )}
+
+
+ {/* Audio player at bottom if applicable */}
+ {defaultTab !== 'model' && defaultTab !== 'video' && audioUrl && (
+
+ )}
+
+ {showAgeCheck &&
}
+
+ )
+}
diff --git a/components/NftPreview.js b/components/NftPreview.js
index 24cc5c4fa..96b35bf87 100644
--- a/components/NftPreview.js
+++ b/components/NftPreview.js
@@ -7,9 +7,10 @@ import { needNftAgeCheck, nftName, nftUrl } from '../utils/nft'
import Tabs from './Tabs'
import LoadingGif from '../public/images/loading.gif'
-import { FaCloudDownloadAlt } from 'react-icons/fa'
+import { FaCloudDownloadAlt, FaExpand } from 'react-icons/fa'
import ReactPannellum from 'react-pannellum'
import AgeCheck from './UI/AgeCheck'
+import NftFullScreenViewer from './NftFullScreenViewer'
const downloadIcon = (
@@ -42,6 +43,7 @@ export default function NftPreview({ nft }) {
const [errored, setErrored] = useState(false)
const [isPanoramic, setIsPanoramic] = useState(false)
const [showAgeCheck, setShowAgeCheck] = useState(false)
+ const [showFullScreen, setShowFullScreen] = useState(false)
const style = {
textAlign: 'center',
@@ -186,6 +188,26 @@ export default function NftPreview({ nft }) {
setShowAgeCheck(true)
}
+ const renderFullScreenButton = () => (
+
+ )
+
return (
<>
{contentTabList.length > 1 && (
@@ -199,7 +221,8 @@ export default function NftPreview({ nft }) {
style={{ margin: 0 }}
/>
-
+
+ {renderFullScreenButton()}
{t('tabs.' + contentTab)} {downloadIcon}
@@ -304,7 +327,8 @@ export default function NftPreview({ nft }) {
>
)}
{contentTabList.length < 2 && defaultUrl && (
-
+
+ {renderFullScreenButton()}
{t('tabs.' + defaultTab)}
{' '}
@@ -319,7 +343,8 @@ export default function NftPreview({ nft }) {
{defaultTab !== 'model' && defaultTab !== 'video' && audioUrl && (
<>
-
+
+ {renderFullScreenButton()}
{t('tabs.audio')} {downloadIcon}
@@ -348,6 +373,14 @@ export default function NftPreview({ nft }) {
)}
{showAgeCheck && }
+
+ {/* Full Screen Viewer */}
+ {showFullScreen && (
+ setShowFullScreen(false)}
+ />
+ )}
>
)
}
diff --git a/styles/components/searchBlock.scss b/styles/components/searchBlock.scss
index 545cd23ea..1a1167005 100644
--- a/styles/components/searchBlock.scss
+++ b/styles/components/searchBlock.scss
@@ -5,6 +5,8 @@
background-color: var(--background-block);
text-align: center;
transition: background-color .5s;
+ position: relative;
+ z-index: 0;
.search-box {
display: inline-block;
position: relative;