Merged in feat/enable-live-preview (pull request #15)

Feat/enable live preview

Approved-by: Simon.Emanuelsson
This commit is contained in:
Christel Westerberg
2024-02-12 13:05:30 +00:00
committed by Simon.Emanuelsson
18 changed files with 385 additions and 30 deletions

View File

@@ -2,3 +2,5 @@ CMS_ACCESS_TOKEN=""
CMS_API_KEY="" CMS_API_KEY=""
CMS_ENVIRONMENT="development" CMS_ENVIRONMENT="development"
CMS_URL="https://eu-graphql.contentstack.com/stacks/${CMS_API_KEY}?environment=${CMS_ENVIRONMENT}" CMS_URL="https://eu-graphql.contentstack.com/stacks/${CMS_API_KEY}?environment=${CMS_ENVIRONMENT}"
CMS_PREVIEW_URL="https://graphql-preview.contentstack.com/stacks/${CMS_API_KEY}?environment=${CMS_ENVIRONMENT}";
CMS_PREVIEW_TOKEN=""

View File

@@ -1,7 +1,7 @@
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import { request } from "@/lib/request" import { request } from "@/lib/request";
import { GetCurrentBlockPage } from "@/lib/graphql/Query/CurrentBlockPage.graphql" import { GetCurrentBlockPage } from "@/lib/graphql/Query/CurrentBlockPage.graphql";
import Aside from "@/components/Current/Aside" import Aside from "@/components/Current/Aside"
import Blocks from "@/components/Current/Blocks" import Blocks from "@/components/Current/Blocks"
@@ -23,10 +23,13 @@ export default async function CurrentContentPage({
throw new Error("Bad URI") throw new Error("Bad URI")
} }
const response = await request<GetCurrentBlockPageData>(GetCurrentBlockPage, { const response = await request<GetCurrentBlockPageData>(
locale: params.lang, GetCurrentBlockPage,
url: searchParams.uri, {
}) locale: params.lang,
url: searchParams.uri,
}
)
if (!response.data?.all_current_blocks_page?.total) { if (!response.data?.all_current_blocks_page?.total) {
console.log("#### DATA ####") console.log("#### DATA ####")
@@ -44,13 +47,18 @@ export default async function CurrentContentPage({
<> <>
<Header lang={params.lang} pathname={searchParams.uri} /> <Header lang={params.lang} pathname={searchParams.uri} />
{images?.totalCount ? <Hero images={images.edges} /> : null} {images?.totalCount ? <Hero images={images.edges} /> : null}
<main <main className="main l-sections-wrapper" id="maincontent" role="main">
className="main l-sections-wrapper" <input
id="maincontent" id="lbl-personalized-areas"
role="main" name="lbl-personalized-areas"
> type="hidden"
<input id="lbl-personalized-areas" name="lbl-personalized-areas" type="hidden" value="" /> value=""
<SubnavMobile breadcrumbs={breadcrumbs} parent={parent} title={page.breadcrumbs.title} /> />
<SubnavMobile
breadcrumbs={breadcrumbs}
parent={parent}
title={page.breadcrumbs.title}
/>
<Preamble <Preamble
breadcrumbs={breadcrumbs} breadcrumbs={breadcrumbs}
breadcrumbParent={parent} breadcrumbParent={parent}

View File

@@ -1,5 +1,5 @@
import "../core.css"; import "../../core.css";
import "../scandic.css"; import "../../scandic.css";
import Footer from "@/components/Current/Footer"; import Footer from "@/components/Current/Footer";
import LangPopup from "@/components/Current/LangPopup"; import LangPopup from "@/components/Current/LangPopup";

View File

@@ -0,0 +1,9 @@
"use client"
export default function Error({ error }: { error: Error }) {
return (
<div>
<h2>Something went wrong!</h2>
</div>
)
}

View File

@@ -0,0 +1,55 @@
import "../../core.css";
import "../../scandic.css";
import Footer from "@/components/Current/Footer";
import LangPopup from "@/components/Current/LangPopup";
import Script from "next/script";
import SkipToMainContent from "@/components/SkipToMainContent";
import type { Metadata } from "next";
import type { LangParams, LayoutArgs } from "@/types/params";
import InitLivePreview from "@/components/Current/LivePreview";
export const metadata: Metadata = {
description: "New web",
title: "Scandic Hotels New Web",
};
export default function RootLayout({
children,
params,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
return (
<html lang={params.lang}>
<head>
<Script
data-cookieconsent="ignore"
src="/Static/dist/js/inline.js?00133e5a37de35c51a5d"
/>
<Script
data-cookieconsent="ignore"
src="/Static/dist/js/main.js?89d0030e1a04b3b46d0b"
/>
<Script
data-cookieconsent="ignore"
src="/Static/dist/js/ng/polyfills.js?1705409330990"
/>
<Script
data-cookieconsent="ignore"
src="/Static/dist/js/ng/runtime.js?1705409330990"
/>
<Script
data-cookieconsent="ignore"
src="/Static/dist/js/ng/main.js?1705409330990"
/>
</head>
<body>
<InitLivePreview />
<LangPopup lang={params.lang} />
<SkipToMainContent lang={params.lang} />
{children}
<Footer lang={params.lang} />
</body>
</html>
)
}

View File

@@ -0,0 +1,8 @@
export default function NotFound() {
return (
<main>
<h2>Not Found</h2>
<p>Could not find requested resource</p>
</main>
)
}

View File

@@ -0,0 +1,58 @@
import { notFound } from "next/navigation"
import { previewRequest } from "@/lib/previewRequest"
import { GetCurrentBlockPage } from "@/lib/graphql/Query/CurrentBlockPage.graphql"
import Aside from "@/components/Current/Aside"
import Blocks from "@/components/Current/Blocks"
import Header from "@/components/Current/Header"
import Hero from "@/components/Current/Hero"
import type { PageArgs, LangParams, UriParams } from "@/types/params"
import type { GetCurrentBlockPageData } from "@/types/requests/currentBlockPage"
import ContentstackLivePreview from "@contentstack/live-preview-utils"
import LoadingPreview from "@/components/Current/LoadingPreview"
export default async function CurrentContentPage({
params,
searchParams,
}: PageArgs<LangParams, UriParams>) {
try {
ContentstackLivePreview.setConfigFromParams(searchParams)
if (!searchParams.uri) {
return <LoadingPreview />
}
const response = await previewRequest<GetCurrentBlockPageData>(
GetCurrentBlockPage,
{ locale: params.lang, uid: searchParams.uri }
)
if (!response.data?.all_current_blocks_page?.total) {
console.log("#### DATA ####")
console.log(response.data)
console.log("SearchParams URI: ", searchParams.uri)
throw new Error("Not found")
}
const page = response.data.all_current_blocks_page.items[0]
const images = page.hero?.imagesConnection
return (
<>
<Header lang={params.lang} pathname={searchParams.uri} />
{images?.totalCount ? <Hero images={images.edges} /> : null}
<main className="main l-sections-wrapper" id="maincontent" role="main">
<h1>{page.title}</h1>
<Blocks blocks={page.blocks} />
<Aside blocks={page.aside} />
</main>
</>
)
} catch (error) {
// TODO: throw 500
console.error(error)
throw new Error("Something went wrong")
}
}

View File

@@ -1,7 +0,0 @@
export default async function Home() {
return (
<main>
<h1>Hello world!</h1>
</main>
);
}

View File

@@ -0,0 +1,14 @@
"use client";
import { useEffect } from "react"
import ContentstackLivePreview from "@contentstack/live-preview-utils"
export default function InitLivePreview() {
useEffect(() => {
if (!ContentstackLivePreview.livePreview) {
ContentstackLivePreview.init()
}
}, []);
return null;
}

View File

@@ -0,0 +1,22 @@
import styles from "./loading.module.css"
export default function LoadingPreview() {
return (
<div className={styles.container}>
<div className={styles.spinner}>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
)
}

View File

@@ -0,0 +1,90 @@
.container {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.spinner {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.spinner div {
transform-origin: 40px 40px;
animation: spinnerAnimation 1.2s linear infinite;
}
.spinner div::after {
content: " ";
display: block;
position: absolute;
top: 3px;
left: 37px;
width: 6px;
height: 18px;
border-radius: 20%;
background: red;
}
.spinner div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -1.1s;
}
.spinner div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -1s;
}
.spinner div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.9s;
}
.spinner div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.8s;
}
.spinner div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.7s;
}
.spinner div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.6s;
}
.spinner div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.5s;
}
.spinner div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.4s;
}
.spinner div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.3s;
}
.spinner div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.2s;
}
.spinner div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.1s;
}
.spinner div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
}
@keyframes spinnerAnimation {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

6
env/schema.mjs vendored
View File

@@ -10,6 +10,8 @@ export const serverSchema = z.object({
CMS_API_KEY: z.string(), CMS_API_KEY: z.string(),
CMS_ENVIRONMENT: z.enum(["development", "production", "staging", "test"]), CMS_ENVIRONMENT: z.enum(["development", "production", "staging", "test"]),
CMS_URL: z.string(), CMS_URL: z.string(),
CMS_PREVIEW_URL: z.string(),
CMS_PREVIEW_TOKEN: z.string(),
NODE_ENV: z.enum(["development", "test", "production"]), NODE_ENV: z.enum(["development", "test", "production"]),
PRINT_QUERY: z.boolean().default(false), PRINT_QUERY: z.boolean().default(false),
}) })
@@ -24,9 +26,11 @@ export const serverEnv = {
CMS_API_KEY: process.env.CMS_API_KEY, CMS_API_KEY: process.env.CMS_API_KEY,
CMS_ENVIRONMENT: process.env.CMS_ENVIRONMENT, CMS_ENVIRONMENT: process.env.CMS_ENVIRONMENT,
CMS_URL: process.env.CMS_URL, CMS_URL: process.env.CMS_URL,
CMS_PREVIEW_URL: process.env.CMS_PREVIEW_URL,
CMS_PREVIEW_TOKEN: process.env.CMS_PREVIEW_TOKEN,
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,
PRINT_QUERY: process.env.PRINT_QUERY, PRINT_QUERY: process.env.PRINT_QUERY,
} };
/** /**
* Specify your client-side environment variables schema here. * Specify your client-side environment variables schema here.

View File

@@ -28,6 +28,10 @@ query GetCurrentBlockPage($locale: String!, $url: String!) {
...Preamble ...Preamble
title title
url url
system {
uid
content_type_uid
}
} }
total total
} }

39
lib/previewRequest.ts Normal file
View File

@@ -0,0 +1,39 @@
import "server-only";
import { request as graphqlRequest } from "graphql-request";
import { env } from "@/env/server.mjs";
import type { Data } from "@/types/request";
import type { DocumentNode } from "graphql";
import ContentstackLivePreview from "@contentstack/live-preview-utils";
export async function previewRequest<T>(
query: string | DocumentNode,
variables?: {}
): Promise<Data<T>> {
try {
const hash = ContentstackLivePreview.hash
if (!hash) {
throw new Error("No hash received")
}
const headers = new Headers({
access_token: env.CMS_ACCESS_TOKEN,
preview_token: env.CMS_PREVIEW_TOKEN,
live_preview: hash,
})
const response = await graphqlRequest<T>({
document: query,
requestHeaders: headers,
url: env.CMS_PREVIEW_URL,
variables,
})
return { data: response }
} catch (error) {
console.error(error);
throw new Error("Something went wrong");
}
}

View File

@@ -1,3 +1,4 @@
import ContentstackLivePreview from "@contentstack/live-preview-utils";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import type { NextRequest } from "next/server"; import type { NextRequest } from "next/server";
@@ -6,17 +7,17 @@ export async function middleware(request: NextRequest) {
// const locales = await fetch(CMS_API, { // const locales = await fetch(CMS_API, {
// locales: true // locales: true
// }) // })
const locales = ["en", "sv", "no", "fi", "da", "de"]; const locales = ["en", "sv", "no", "fi", "da", "de"]
const locale = locales.find( const locale = locales.find(
(locale) => (locale) =>
request.nextUrl.pathname.startsWith(`/${locale}/`) || request.nextUrl.pathname.startsWith(`/${locale}/`) ||
request.nextUrl.pathname === `/${locale}` request.nextUrl.pathname === `/${locale}`
); )
if (!locale) { if (!locale) {
//return <LocalePicker /> //return <LocalePicker />
return Response.json("Not found!!!", { status: 404 }); return Response.json("Not found!!!", { status: 404 })
} }
// const data = await fetch(CMS_API, { // const data = await fetch(CMS_API, {
@@ -25,14 +26,23 @@ export async function middleware(request: NextRequest) {
// }).json() // }).json()
//const contentType = data.response.meta.contentType; //const contentType = data.response.meta.contentType;
const contentType = "currentContentPage"; const contentType = "currentContentPage"
const pathNameWithoutLocale = request.nextUrl.pathname.replace( const pathNameWithoutLocale = request.nextUrl.pathname.replace(
`/${locale}`, `/${locale}`,
"" ""
); )
const searchParams = new URLSearchParams(request.nextUrl.searchParams) const searchParams = new URLSearchParams(request.nextUrl.searchParams)
if (request.nextUrl.pathname.includes("preview")) {
searchParams.set("uri", pathNameWithoutLocale.replace("/preview", ""))
return NextResponse.rewrite(
new URL(`/${locale}/preview?${searchParams.toString()}`, request.url)
)
}
searchParams.set("uri", pathNameWithoutLocale) searchParams.set("uri", pathNameWithoutLocale)
switch (contentType) { switch (contentType) {
@@ -42,9 +52,9 @@ export async function middleware(request: NextRequest) {
`/${locale}/current-content-page?${searchParams.toString()}`, `/${locale}/current-content-page?${searchParams.toString()}`,
request.url request.url
) )
); )
} }
return NextResponse.redirect(new URL("/home", request.url)); return NextResponse.redirect(new URL("/home", request.url))
} }
// See "Matching Paths" below to learn more // See "Matching Paths" below to learn more

38
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "web", "name": "web",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@contentstack/live-preview-utils": "^1.4.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"graphql": "16.8.1", "graphql": "16.8.1",
"graphql-request": "6.1.0", "graphql-request": "6.1.0",
@@ -51,6 +52,17 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@contentstack/live-preview-utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@contentstack/live-preview-utils/-/live-preview-utils-1.4.0.tgz",
"integrity": "sha512-74N9ACoUwSrvmbtqoy8CkX7H/OmA3cjnOmaKMq6qoVI1r9kJjO+gqQevBPxs17nurbNL2XjXU56hbHZagWL4nw==",
"dependencies": {
"just-camel-case": "^4.0.2",
"morphdom": "^2.6.1",
"mustache": "^4.2.0",
"uuid": "^8.3.2"
}
},
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -2498,6 +2510,11 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/just-camel-case": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/just-camel-case/-/just-camel-case-4.0.2.tgz",
"integrity": "sha512-df6QI/EIq+6uHe/wtaa9Qq7/pp4wr4pJC/r1+7XhVL6m5j03G6h9u9/rIZr8rDASX7CxwDPQnZjffCo2e6PRLw=="
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -2625,12 +2642,25 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/morphdom": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.2.tgz",
"integrity": "sha512-Dqb/lHFyTi7SZpY0a5R4I/0Edo+iPMbaUexsHHsLAByyixCDiLHPHyVoKVmrpL0THcT7V9Cgev9y21TQYq6wQg=="
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true "dev": true
}, },
"node_modules/mustache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
"bin": {
"mustache": "bin/mustache"
}
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.7", "version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@@ -3681,6 +3711,14 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",

View File

@@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"@contentstack/live-preview-utils": "^1.4.0",
"graphql": "16.8.1", "graphql": "16.8.1",
"graphql-request": "6.1.0", "graphql-request": "6.1.0",
"graphql-tag": "2.12.6", "graphql-tag": "2.12.6",