add 360 photosphere element support

This commit is contained in:
2026-04-15 23:43:47 +02:00
parent d0a886a4ae
commit 3dd2c056b8
9 changed files with 314 additions and 34 deletions
+39 -5
View File
@@ -16,11 +16,12 @@
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"graphql": "^16.13.1",
"next": "16.1.6",
"next": "^16.1.6",
"nuqs": "^2.8.9",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-intersection-observer": "^10.0.3",
"react-photo-sphere-viewer": "^6.2.3",
"sass": "^1.97.3",
"sharp": "^0.34.5",
"swiper": "^12.1.2",
@@ -3158,6 +3159,16 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@photo-sphere-viewer/core": {
"version": "5.14.1",
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.14.1.tgz",
"integrity": "sha512-qrwUudrX9YZms4c2shlY/H3jUP0oh9FyGEqIDr/95ulNZgKbhQ6C/i8zDQ4j8ooFR4+z5FDORQtGvLgPyX8VCA==",
"license": "MIT",
"peer": true,
"dependencies": {
"three": "^0.179.0"
}
},
"node_modules/@repeaterjs/repeater": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz",
@@ -7504,7 +7515,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7884,7 +7894,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -7958,8 +7967,26 @@
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-photo-sphere-viewer": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/react-photo-sphere-viewer/-/react-photo-sphere-viewer-6.2.3.tgz",
"integrity": "sha512-VzG0aY9CI8OIQjdIoJCjYF1QlnLFpN2pM+zKm1JrpAKQrBZ6B+Uxy94vpVQkGDERgn8FWE0+LIntTgAr60pLyQ==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^5.0.1"
},
"engines": {
"node": ">=16",
"npm": ">=8"
},
"peerDependencies": {
"@photo-sphere-viewer/core": ">=5.13.1",
"prop-types": "^15.5.4",
"react": ">=16.0.0",
"react-dom": ">=16.0.0"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
@@ -8811,6 +8838,13 @@
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/three": {
"version": "0.179.1",
"resolved": "https://registry.npmjs.org/three/-/three-0.179.1.tgz",
"integrity": "sha512-5y/elSIQbrvKOISxpwXCR4sQqHtGiOI+MKLc3SsBdDXA2hz3Mdp3X59aUp8DyybMa34aeBwbFTpdoLJaUDEWSw==",
"license": "MIT",
"peer": true
},
"node_modules/timeout-signal": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/timeout-signal/-/timeout-signal-2.0.0.tgz",
+1
View File
@@ -23,6 +23,7 @@
"react": "19.2.4",
"react-dom": "19.2.4",
"react-intersection-observer": "^10.0.3",
"react-photo-sphere-viewer": "^6.2.3",
"sass": "^1.97.3",
"sharp": "^0.34.5",
"swiper": "^12.1.2",
+4
View File
@@ -6,6 +6,7 @@ import { FeaturedBlock } from "./FeaturedBlock";
import { AccordionBlock } from "./AccordionBlock";
import { EmbedBlock } from "./EmbedBlock";
import { FactBoxBlock } from "./FactBoxBlock";
import { PhotoSphereBlock } from "./PhotoSphereBlock";
import { PageSectionBlock, PageSectionNavigationBlock } from "./PageSection";
import { ContactSectionBlock, ContactSubsectionBlock } from "./ContactSection";
import { ContactListBlock } from "./ContactListBlock";
@@ -44,6 +45,9 @@ export const Blocks = ({ blocks, pageContent }: { blocks: any, pageContent?: boo
case "FactBoxBlock":
return <FactBoxBlock key={block.id} block={block} />;
break;
case "PhotoSphereBlock":
return <PhotoSphereBlock key={block.id} block={block} />;
break;
case "PageSectionBlock":
return <PageSectionBlock key={block.id} block={block} />;
break;
@@ -0,0 +1,65 @@
"use client";
import dynamic from "next/dynamic";
import { PhotoSphereBlock as PhotoSphereBlockType } from "@/gql/graphql";
import styles from "./photoSphereBlock.module.scss";
const ReactPhotoSphereViewer = dynamic(
() =>
import("react-photo-sphere-viewer").then(
(mod) => mod.ReactPhotoSphereViewer
),
{
ssr: false,
loading: () => (
<div className={styles.loading} aria-busy="true" aria-label="Laster 360°-bilde">
<span className={styles.loadingText}>Laster 360°-bilde</span>
</div>
),
}
);
type PhotoSphereBlockTypeWithAlias = PhotoSphereBlockType & {
photoSphereImage?: PhotoSphereBlockType["image"];
photoSphereTitle?: string | null;
};
export const PhotoSphereBlock = ({
block,
}: {
block: PhotoSphereBlockTypeWithAlias;
}) => {
const image = block.photoSphereImage ?? block.image;
if (!image?.url) {
return <></>;
}
return (
<figure className={styles.photoSphereWrapper}>
<div
className={styles.photoSphereViewer}
role="img"
aria-label={block.photoSphereTitle ?? image.alt ?? "360°-bilde"}
>
<ReactPhotoSphereViewer
src={image.url}
height="500px"
width="100%"
navbar={["zoom", "fullscreen"]}
littlePlanet={false}
touchmoveTwoFingers
/>
<noscript>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={image.url}
alt={block.photoSphereTitle ?? image.alt ?? "360°-bilde"}
className={styles.fallbackImage}
/>
</noscript>
</div>
{block.photoSphereTitle && <figcaption>{block.photoSphereTitle}</figcaption>}
</figure>
);
};
@@ -0,0 +1,42 @@
.photoSphereWrapper {
max-width: var(--size-width-p);
margin: 0 auto var(--spacing-m);
}
.photoSphereViewer {
width: 100%;
overflow: hidden;
border-radius: 2px;
background: var(--color-betongGray);
position: relative;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 500px;
width: 100%;
background: var(--color-background-secondary);
}
.loadingText {
font-size: var(--font-size-body);
color: var(--color-text-secondary, currentColor);
}
.photoSphereWrapper figcaption {
width: 100%;
max-width: var(--size-width-p);
margin: 0 auto;
padding: var(--spacing-xs) 0 var(--spacing-s);
font-size: var(--font-size-caption);
line-height: 1.4;
opacity: .8;
}
.fallbackImage {
width: 100%;
height: auto;
display: block;
}
+3 -3
View File
@@ -40,7 +40,7 @@ type Documents = {
"\n fragment SponsorsPage on SponsorsPage {\n ... on SponsorsPage {\n title\n seoTitle\n searchDescription\n lead\n body {\n ...Blocks\n }\n sponsors {\n ... on SponsorBlock {\n id\n name\n logo {\n ...Image\n }\n text\n website\n }\n }\n }\n }\n": typeof types.SponsorsPageFragmentDoc,
"\n query venueRentalIndex {\n index: venueRentalIndex {\n ... on VenueRentalIndex {\n ...VenueRentalIndex\n }\n }\n venues: pages(contentType: \"venues.VenuePage\", limit: 100) {\n ... on VenuePage {\n ...Venue\n }\n }\n }\n": typeof types.VenueRentalIndexDocument,
"\n fragment VenueRentalIndex on VenueRentalIndex {\n ... on VenueRentalIndex {\n title\n seoTitle\n searchDescription\n lead\n body {\n ...Blocks\n }\n }\n }\n": typeof types.VenueRentalIndexFragmentDoc,
"\n fragment LeafBlocks on StreamFieldInterface {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n ... on ImageWithTextBlock {\n image {\n ...Image\n }\n imageFormat\n text\n }\n ... on ImageSliderBlock {\n images {\n ... on ImageSliderItemBlock {\n image {\n ...Image\n }\n text\n }\n }\n }\n ... on HorizontalRuleBlock {\n color\n }\n ... on FeaturedBlock {\n title\n featuredBlockText: text\n linkText\n imagePosition\n backgroundColor\n featuredPage {\n contentType\n pageType\n url\n ... on EventPage {\n featuredImage {\n ...Image\n }\n }\n ... on NewsPage {\n featuredImage {\n ...Image\n }\n }\n }\n featuredImageOverride {\n ...Image\n }\n }\n ... on ContactListBlock {\n items {\n blockType\n ... on ContactEntityBlock {\n contactEntity {\n ...ContactEntity\n }\n }\n }\n }\n ... on EmbedBlock {\n url\n embed\n rawEmbed\n }\n ... on FactBoxBlock {\n backgroundColor\n factBoxBody: body\n }\n }\n": typeof types.LeafBlocksFragmentDoc,
"\n fragment LeafBlocks on StreamFieldInterface {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n ... on ImageWithTextBlock {\n image {\n ...Image\n }\n imageFormat\n text\n }\n ... on ImageSliderBlock {\n images {\n ... on ImageSliderItemBlock {\n image {\n ...Image\n }\n text\n }\n }\n }\n ... on HorizontalRuleBlock {\n color\n }\n ... on FeaturedBlock {\n title\n featuredBlockText: text\n linkText\n imagePosition\n backgroundColor\n featuredPage {\n contentType\n pageType\n url\n ... on EventPage {\n featuredImage {\n ...Image\n }\n }\n ... on NewsPage {\n featuredImage {\n ...Image\n }\n }\n }\n featuredImageOverride {\n ...Image\n }\n }\n ... on ContactListBlock {\n items {\n blockType\n ... on ContactEntityBlock {\n contactEntity {\n ...ContactEntity\n }\n }\n }\n }\n ... on EmbedBlock {\n url\n embed\n rawEmbed\n }\n ... on FactBoxBlock {\n backgroundColor\n factBoxBody: body\n }\n ... on PhotoSphereBlock {\n photoSphereImage: image {\n ...Image\n }\n photoSphereTitle: title\n }\n }\n": typeof types.LeafBlocksFragmentDoc,
"\n fragment OneLevelOfBlocks on StreamFieldInterface {\n ...LeafBlocks\n ... on AccordionBlock {\n heading\n body {\n ...LeafBlocks\n }\n }\n ... on PageSectionBlock {\n title\n backgroundColor\n icon\n body {\n ...LeafBlocks\n }\n }\n }\n": typeof types.OneLevelOfBlocksFragmentDoc,
"\n fragment Blocks on StreamFieldInterface {\n ... on AccordionBlock {\n heading\n body {\n ...OneLevelOfBlocks\n }\n }\n ... on PageSectionBlock {\n title\n backgroundColor\n icon\n body {\n ...OneLevelOfBlocks\n }\n }\n ... on ContactSectionBlock {\n title\n text\n blocks {\n ... on ContactSubsectionBlock {\n title\n text\n blocks {\n ...OneLevelOfBlocks\n }\n }\n ...OneLevelOfBlocks\n }\n }\n ...OneLevelOfBlocks\n }\n": typeof types.BlocksFragmentDoc,
"\n fragment Image on CustomImage {\n id\n url\n width\n height\n alt\n attribution\n }\n": typeof types.ImageFragmentDoc,
@@ -84,7 +84,7 @@ const documents: Documents = {
"\n fragment SponsorsPage on SponsorsPage {\n ... on SponsorsPage {\n title\n seoTitle\n searchDescription\n lead\n body {\n ...Blocks\n }\n sponsors {\n ... on SponsorBlock {\n id\n name\n logo {\n ...Image\n }\n text\n website\n }\n }\n }\n }\n": types.SponsorsPageFragmentDoc,
"\n query venueRentalIndex {\n index: venueRentalIndex {\n ... on VenueRentalIndex {\n ...VenueRentalIndex\n }\n }\n venues: pages(contentType: \"venues.VenuePage\", limit: 100) {\n ... on VenuePage {\n ...Venue\n }\n }\n }\n": types.VenueRentalIndexDocument,
"\n fragment VenueRentalIndex on VenueRentalIndex {\n ... on VenueRentalIndex {\n title\n seoTitle\n searchDescription\n lead\n body {\n ...Blocks\n }\n }\n }\n": types.VenueRentalIndexFragmentDoc,
"\n fragment LeafBlocks on StreamFieldInterface {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n ... on ImageWithTextBlock {\n image {\n ...Image\n }\n imageFormat\n text\n }\n ... on ImageSliderBlock {\n images {\n ... on ImageSliderItemBlock {\n image {\n ...Image\n }\n text\n }\n }\n }\n ... on HorizontalRuleBlock {\n color\n }\n ... on FeaturedBlock {\n title\n featuredBlockText: text\n linkText\n imagePosition\n backgroundColor\n featuredPage {\n contentType\n pageType\n url\n ... on EventPage {\n featuredImage {\n ...Image\n }\n }\n ... on NewsPage {\n featuredImage {\n ...Image\n }\n }\n }\n featuredImageOverride {\n ...Image\n }\n }\n ... on ContactListBlock {\n items {\n blockType\n ... on ContactEntityBlock {\n contactEntity {\n ...ContactEntity\n }\n }\n }\n }\n ... on EmbedBlock {\n url\n embed\n rawEmbed\n }\n ... on FactBoxBlock {\n backgroundColor\n factBoxBody: body\n }\n }\n": types.LeafBlocksFragmentDoc,
"\n fragment LeafBlocks on StreamFieldInterface {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n ... on ImageWithTextBlock {\n image {\n ...Image\n }\n imageFormat\n text\n }\n ... on ImageSliderBlock {\n images {\n ... on ImageSliderItemBlock {\n image {\n ...Image\n }\n text\n }\n }\n }\n ... on HorizontalRuleBlock {\n color\n }\n ... on FeaturedBlock {\n title\n featuredBlockText: text\n linkText\n imagePosition\n backgroundColor\n featuredPage {\n contentType\n pageType\n url\n ... on EventPage {\n featuredImage {\n ...Image\n }\n }\n ... on NewsPage {\n featuredImage {\n ...Image\n }\n }\n }\n featuredImageOverride {\n ...Image\n }\n }\n ... on ContactListBlock {\n items {\n blockType\n ... on ContactEntityBlock {\n contactEntity {\n ...ContactEntity\n }\n }\n }\n }\n ... on EmbedBlock {\n url\n embed\n rawEmbed\n }\n ... on FactBoxBlock {\n backgroundColor\n factBoxBody: body\n }\n ... on PhotoSphereBlock {\n photoSphereImage: image {\n ...Image\n }\n photoSphereTitle: title\n }\n }\n": types.LeafBlocksFragmentDoc,
"\n fragment OneLevelOfBlocks on StreamFieldInterface {\n ...LeafBlocks\n ... on AccordionBlock {\n heading\n body {\n ...LeafBlocks\n }\n }\n ... on PageSectionBlock {\n title\n backgroundColor\n icon\n body {\n ...LeafBlocks\n }\n }\n }\n": types.OneLevelOfBlocksFragmentDoc,
"\n fragment Blocks on StreamFieldInterface {\n ... on AccordionBlock {\n heading\n body {\n ...OneLevelOfBlocks\n }\n }\n ... on PageSectionBlock {\n title\n backgroundColor\n icon\n body {\n ...OneLevelOfBlocks\n }\n }\n ... on ContactSectionBlock {\n title\n text\n blocks {\n ... on ContactSubsectionBlock {\n title\n text\n blocks {\n ...OneLevelOfBlocks\n }\n }\n ...OneLevelOfBlocks\n }\n }\n ...OneLevelOfBlocks\n }\n": types.BlocksFragmentDoc,
"\n fragment Image on CustomImage {\n id\n url\n width\n height\n alt\n attribution\n }\n": types.ImageFragmentDoc,
@@ -223,7 +223,7 @@ export function graphql(source: "\n fragment VenueRentalIndex on VenueRentalInd
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment LeafBlocks on StreamFieldInterface {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n ... on ImageWithTextBlock {\n image {\n ...Image\n }\n imageFormat\n text\n }\n ... on ImageSliderBlock {\n images {\n ... on ImageSliderItemBlock {\n image {\n ...Image\n }\n text\n }\n }\n }\n ... on HorizontalRuleBlock {\n color\n }\n ... on FeaturedBlock {\n title\n featuredBlockText: text\n linkText\n imagePosition\n backgroundColor\n featuredPage {\n contentType\n pageType\n url\n ... on EventPage {\n featuredImage {\n ...Image\n }\n }\n ... on NewsPage {\n featuredImage {\n ...Image\n }\n }\n }\n featuredImageOverride {\n ...Image\n }\n }\n ... on ContactListBlock {\n items {\n blockType\n ... on ContactEntityBlock {\n contactEntity {\n ...ContactEntity\n }\n }\n }\n }\n ... on EmbedBlock {\n url\n embed\n rawEmbed\n }\n ... on FactBoxBlock {\n backgroundColor\n factBoxBody: body\n }\n }\n"): (typeof documents)["\n fragment LeafBlocks on StreamFieldInterface {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n ... on ImageWithTextBlock {\n image {\n ...Image\n }\n imageFormat\n text\n }\n ... on ImageSliderBlock {\n images {\n ... on ImageSliderItemBlock {\n image {\n ...Image\n }\n text\n }\n }\n }\n ... on HorizontalRuleBlock {\n color\n }\n ... on FeaturedBlock {\n title\n featuredBlockText: text\n linkText\n imagePosition\n backgroundColor\n featuredPage {\n contentType\n pageType\n url\n ... on EventPage {\n featuredImage {\n ...Image\n }\n }\n ... on NewsPage {\n featuredImage {\n ...Image\n }\n }\n }\n featuredImageOverride {\n ...Image\n }\n }\n ... on ContactListBlock {\n items {\n blockType\n ... on ContactEntityBlock {\n contactEntity {\n ...ContactEntity\n }\n }\n }\n }\n ... on EmbedBlock {\n url\n embed\n rawEmbed\n }\n ... on FactBoxBlock {\n backgroundColor\n factBoxBody: body\n }\n }\n"];
export function graphql(source: "\n fragment LeafBlocks on StreamFieldInterface {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n ... on ImageWithTextBlock {\n image {\n ...Image\n }\n imageFormat\n text\n }\n ... on ImageSliderBlock {\n images {\n ... on ImageSliderItemBlock {\n image {\n ...Image\n }\n text\n }\n }\n }\n ... on HorizontalRuleBlock {\n color\n }\n ... on FeaturedBlock {\n title\n featuredBlockText: text\n linkText\n imagePosition\n backgroundColor\n featuredPage {\n contentType\n pageType\n url\n ... on EventPage {\n featuredImage {\n ...Image\n }\n }\n ... on NewsPage {\n featuredImage {\n ...Image\n }\n }\n }\n featuredImageOverride {\n ...Image\n }\n }\n ... on ContactListBlock {\n items {\n blockType\n ... on ContactEntityBlock {\n contactEntity {\n ...ContactEntity\n }\n }\n }\n }\n ... on EmbedBlock {\n url\n embed\n rawEmbed\n }\n ... on FactBoxBlock {\n backgroundColor\n factBoxBody: body\n }\n ... on PhotoSphereBlock {\n photoSphereImage: image {\n ...Image\n }\n photoSphereTitle: title\n }\n }\n"): (typeof documents)["\n fragment LeafBlocks on StreamFieldInterface {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n ... on ImageWithTextBlock {\n image {\n ...Image\n }\n imageFormat\n text\n }\n ... on ImageSliderBlock {\n images {\n ... on ImageSliderItemBlock {\n image {\n ...Image\n }\n text\n }\n }\n }\n ... on HorizontalRuleBlock {\n color\n }\n ... on FeaturedBlock {\n title\n featuredBlockText: text\n linkText\n imagePosition\n backgroundColor\n featuredPage {\n contentType\n pageType\n url\n ... on EventPage {\n featuredImage {\n ...Image\n }\n }\n ... on NewsPage {\n featuredImage {\n ...Image\n }\n }\n }\n featuredImageOverride {\n ...Image\n }\n }\n ... on ContactListBlock {\n items {\n blockType\n ... on ContactEntityBlock {\n contactEntity {\n ...ContactEntity\n }\n }\n }\n }\n ... on EmbedBlock {\n url\n embed\n rawEmbed\n }\n ... on FactBoxBlock {\n backgroundColor\n factBoxBody: body\n }\n ... on PhotoSphereBlock {\n photoSphereImage: image {\n ...Image\n }\n photoSphereTitle: title\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
+134 -26
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -164,6 +164,12 @@ const LeafBlocksFragmentDefinition = graphql(`
backgroundColor
factBoxBody: body
}
... on PhotoSphereBlock {
photoSphereImage: image {
...Image
}
photoSphereTitle: title
}
}
`);