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
4 changes: 3 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body>{children}</body>
<body>
<main className="max-w-(--breakpoint-md) mx-auto">{children}</main>
</body>
</html>
);
}
143 changes: 30 additions & 113 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,114 +1,31 @@
"use client";
import DOMPurify from "dompurify";
import Image from "next/image";
import ContentstackLivePreview from "@contentstack/live-preview-utils";
import { getPage, initLivePreview } from "@/lib/contentstack";
import { useEffect, useState } from "react";
import { Page } from "@/lib/types";

export default function Home() {
const [page, setPage] = useState<Page>();

const getContent = async () => {
const page = await getPage("/");
setPage(page as Page);
};

useEffect(() => {
initLivePreview();
ContentstackLivePreview.onEntryChange(getContent);
}, []);

return (
<main className="max-w-(--breakpoint-md) mx-auto">
<section className="p-4">
{page?.title ? (
<h1
className="text-4xl font-bold mb-4"
{...(page?.$ && page?.$.title)}
>
{page?.title}
</h1>
) : null}

{page?.description ? (
<p className="mb-4" {...(page?.$ && page?.$.description)}>
{page?.description}
</p>
) : null}

{page?.image ? (
<Image
className="mb-4"
width={768}
height={414}
src={page?.image.url}
alt={page?.image.title}
{...(page?.image.$ && page?.image.$.url)}
/>
) : null}

{page?.rich_text ? (
<div
{...(page?.$ && page?.$.rich_text)}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(page?.rich_text),
}}
/>
) : null}

<div
className="space-y-8 max-w-full mt-4"
{...(page?.$ && page?.$.blocks)}
>
{page?.blocks?.map((item, index) => {
const { block } = item;
const isImageLeft = block.layout === "image_left";

return (
<div
key={`${block}-${index}`}
{...(page?.$ && page?.$[`blocks__${index}`])} // Adding editable tags if available
className={`flex flex-col md:flex-row items-center space-y-4 md:space-y-0 bg-white ${
isImageLeft ? "md:flex-row" : "md:flex-row-reverse" // Adjusting the layout based on the block's layout property
}`}
>
<div className="w-full md:w-1/2">
{block.image ? (
<Image
src={block.image.url}
alt={block.image.title}
width={200}
height={112}
className="w-full"
{...(block?.image.$ && block?.image.$.url)}
/>
) : null}
</div>
<div className="w-full md:w-1/2 p-4">
{block.title ? (
<h2
className="text-2xl font-bold"
{...(block?.$ && block?.$.title)}
>
{block.title}
</h2>
) : null}
{block.copy ? (
<div
{...(block?.$ && block?.$.copy)}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(block.copy),
}}
className="prose"
/>
) : null}
</div>
</div>
);
})}
</div>
</section>
</main>
);
// Importing React Suspense for handling async client components
import { Suspense } from "react";
// Importing function to fetch page data and preview mode checker from Contentstack utilities
import { getPage, isPreview } from "@/lib/contentstack";
// Importing the Page component to render static content
import Page from "@/components/Page";
// Importing the Preview component to render live preview content
import Preview from "@/components/Preview";

// Home page component - serves as the main entry point for the application
// This is an async server component that can fetch data at build time or request time
export default async function Home() {
// Check if the application is running in preview mode
// Preview mode enables live editing capabilities for content creators
if (isPreview) {
// Return the Preview component wrapped in Suspense boundary
// Suspense is required for useSearchParams() hook in Next.js 15
// The path "/" represents the home page URL in Contentstack
return (
<Suspense fallback={<div className="flex flex-col items-center justify-center h-screen"><p>Loading...</p></div>}>
<Preview path="/" />
</Suspense>
);
}

// In production mode, fetch the page data server-side for better performance
const page = await getPage("/"); // Fetch home page content from Contentstack

// Return the static Page component with the pre-fetched data
return <Page page={page} />;
}
129 changes: 129 additions & 0 deletions components/Page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Importing DOMPurify for sanitizing HTML content to prevent XSS attacks
import DOMPurify from "isomorphic-dompurify";
// Importing Next.js optimized Image component for better performance
import Image from "next/image";
// Importing the Page type definition from our types file
import { Page } from "@/lib/types";
// Importing Visual Builder class for empty block handling in Contentstack Live Preview
import { VB_EmptyBlockParentClass } from "@contentstack/live-preview-utils";

// Interface defining the props for the ContentDisplay component
interface ContentDisplayProps {
page: Page | undefined; // Page data that may be undefined during loading
}

// Main component for displaying page content with Contentstack Live Preview support
export default function ContentDisplay({ page }: ContentDisplayProps) {
return (
<section className="p-4">
{/* Display page title if it exists */}
{page?.title ? (
<h1
className="text-4xl font-bold mb-4 text-center"
// Spread live preview attributes for editing capability in Contentstack
{...(page?.$ && page?.$.title)}
>
{page?.title} with Next
</h1>
) : null}

{/* Display page description if it exists */}
{page?.description ? (
<p className="mb-4 text-center" {...(page?.$ && page?.$.description)}>
{page?.description}
</p>
) : null}

{/* Display hero image if it exists */}
{page?.image ? (
<Image
className="mb-4"
width={768}
height={414}
src={page?.image.url}
alt={page?.image.title}
// Spread live preview attributes for the image field
{...(page?.image?.$ && page?.image?.$.url)}
/>
) : null}

{/* Display rich text content if it exists, sanitized for security */}
{page?.rich_text ? (
<div
{...(page?.$ && page?.$.rich_text)}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(page?.rich_text), // Sanitize HTML to prevent XSS attacks
}}
/>
) : null}

{/*
Container for modular blocks with Visual Builder support
Adds empty block class when no blocks exist for better editing experience
*/}
<div
className={`space-y-8 max-w-full mt-4 ${
!page?.blocks || page.blocks.length === 0
? VB_EmptyBlockParentClass // Special class for empty state in Visual Builder
: ""
}`}
{...(page?.$ && page?.$.blocks)}
>
{/* Map through blocks array to render each modular content block */}
{page?.blocks?.map((item, index) => {
const { block } = item; // Extract block data from item
const isImageLeft = block.layout === "image_left"; // Determine layout direction

return (
<div
key={block._metadata?.uid || `block-${index}`} // Use unique identifier as key for React
{...(page?.$ && page?.$[`blocks__${index}`])} // Live preview attributes for each block
className={`flex flex-col md:flex-row items-center space-y-4 md:space-y-0 bg-white ${
isImageLeft ? "md:flex-row" : "md:flex-row-reverse" // Conditional layout based on block settings
}`}
>
{/* Image container - takes half width on medium screens and up */}
<div className="w-full md:w-1/2">
{block.image ? (
<Image
key={`image-${block._metadata?.uid || index}`} // Unique key for image
src={block.image.url}
alt={block.image.title}
width={200}
height={112}
className="w-full"
{...(block?.$ && block?.$.image)} // Live preview attributes for image
/>
) : null}
</div>

{/* Content container - takes half width on medium screens and up */}
<div className="w-full md:w-1/2 p-4">
{/* Block title */}
{block.title ? (
<h2
className="text-2xl font-bold"
{...(block?.$ && block?.$.title)} // Live preview attributes for title
>
{block.title}
</h2>
) : null}

{/* Block rich text content, sanitized for security */}
{block.copy ? (
<div
{...(block?.$ && block?.$.copy)} // Live preview attributes for copy
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(block.copy), // Sanitize HTML content
}}
className="prose" // Apply prose styling for better typography
/>
) : null}
</div>
</div>
);
})}
</div>
</section>
);
}
55 changes: 55 additions & 0 deletions components/Preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Client-side component directive for Next.js
"use client";

// Importing React hooks for state management and side effects
import { useState, useEffect, useCallback } from "react";
// Importing Next.js hook to access URL search parameters
import { useSearchParams } from "next/navigation";
// Importing Contentstack Live Preview utilities for real-time content updates
import ContentstackLivePreview from "@contentstack/live-preview-utils";
// Importing functions to fetch page data and initialize live preview
import { getPage, initLivePreview } from "@/lib/contentstack";
// Importing Page type definition with alias to avoid naming conflicts
import type { Page as PageProps } from "@/lib/types";
// Importing the Page component to render the content
import Page from "./Page";

// Loading state component displayed while content is being fetched
const LoadingState = () => (
<div className="flex flex-col items-center justify-center h-screen">
<p>Loading...</p>
</div>
);

// Preview component that handles live preview functionality for Contentstack
export default function Preview({ path }: { path: string }) {
// Get search parameters from the current URL
const searchParams = useSearchParams();
// Extract preview_timestamp query parameter
const previewTimestamp = searchParams.get("preview_timestamp");

// State to store the fetched page data
const [page, setPage] = useState<PageProps>();

// Memoized function to fetch content data based on the current path
// useCallback prevents unnecessary re-renders when path doesn't change
const getContent = useCallback(async () => {
const data = await getPage(path, previewTimestamp || undefined); // Fetch page data from Contentstack
setPage(data); // Update state with fetched data
}, [path]); // Dependency array - function recreated only when path changes

// Effect hook to initialize live preview and set up content change listener
useEffect(() => {
initLivePreview(); // Initialize Contentstack Live Preview functionality
// Set up listener for content changes in the Contentstack interface
ContentstackLivePreview.onEntryChange(getContent); // Refetch content when changes occur
}, [path]); // Re-run effect when path changes

// Show loading state while page data is being fetched
if (!page) {
return <LoadingState />;
}

// Render the Page component with the fetched page data
return <Page page={page as PageProps} />;
}
Loading