Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
fileignoreconfig:
- filename: package-lock.json
checksum: feeab6b3e3a58b1347b5d2e00cb66be4d5f6277e3386837043e733145287e3aa
- filename: types/app.ts
checksum: a6a26902b0d3734888aea634893c96bcaec57af18ad6c531de5a85169e2a9f07
- filename: types/common.ts
checksum: 906230e145a241b70f5ba96ba1cb6817ab00c1c4513c4a6be98a001f1706bff0
- filename: .github/workflows/secrets-scan.yml
ignore_detectors:
- filecontent
Expand Down
18 changes: 14 additions & 4 deletions MainLayout/MainLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
'use client'

import React, { useEffect, useState } from 'react'
import { Footer, Header } from '@/components'
import { ConsentForm, Footer, Header } from '@/components'
import { App } from '@/types'

import useRouterHook from '@/hooks/useRouterHook'
import { LocaleContext, usePersonalization } from '@/context'
import { footerJsonRtePathIncludes, footerReferenceIncludes, getEntries, navigationReferenceIncludes } from '@/services'
import { footerJsonRtePathIncludes, footerReferenceIncludes, getEntries, navigationReferenceIncludes, userFormJsonRtePathIncludes, userFormReferenceIncludes } from '@/services'
import { onEntryChange } from '@/config'

const MainLayout: React.FC<App.MainLayout> = (
props: React.PropsWithChildren<App.MainLayout>
) => {

const [webConfig, setWebConfig] = useState<App.WebConfig>()
const { locale } = useRouterHook()
const {personalizationSDK} = usePersonalization()
Expand All @@ -21,9 +21,11 @@ const MainLayout: React.FC<App.MainLayout> = (
try {
const refUids = [
...navigationReferenceIncludes,
...footerReferenceIncludes
...footerReferenceIncludes,
...userFormReferenceIncludes
]
const jsonRtePaths = [
...userFormJsonRtePathIncludes,
...footerJsonRtePathIncludes
]

Expand Down Expand Up @@ -72,6 +74,14 @@ const MainLayout: React.FC<App.MainLayout> = (
logo={webConfig.logo}
/>
}
{/* sticky cookie consent from */}
{webConfig?.consent_modal && <ConsentForm
{...webConfig.consent_modal}
$={{
consent_modal: webConfig?.$?.consent_modal ,
...webConfig?.consent_modal?.$
}}
/>}
</LocaleContext.Provider>}
</>
)
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,11 @@ Refer to the [Bootstrap command documentation](https://www.contentstack.com/docs
- [Contentstack documentation](https://www.contentstack.com/docs/)
- [Region support documentation](https://www.contentstack.com/docs/developers/selecting-region-in-contentstack-starter-apps)
- [Contentstack Typescript SDK](https://www.contentstack.com/docs/developers/sdks/content-delivery-sdk/typescript/reference)
- [Contentstack Personalize SDK](https://www.contentstack.com/docs/personalize)
- [Contentstack Personalize SDK](https://www.contentstack.com/docs/personalize), [Personalize Overview](https://www.contentstack.com/docs/personalize#personalize-overview)
- Visual Experince :
- [Live Preview](https://www.contentstack.com/docs/content-managers/author-content/about-live-preview/)
- [Timeline](https://www.contentstack.com/docs/content-managers/timeline/)
- [Visual Builder](https://www.contentstack.com/docs/content-managers/visual-builder/)
- [Preview Sharing](https://www.contentstack.com/docs/content-managers/preview-sharing)

- [Next.js](https://learnnextjs.com/)
46 changes: 6 additions & 40 deletions app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,16 @@
import type { Metadata } from 'next'
import { Inter, Roboto_Condensed } from 'next/font/google'

import { MainLayout } from '@/MainLayout'
import 'slick-carousel/slick/slick.css'
import 'slick-carousel/slick/slick-theme.css'
import '../globals.css'
import '/node_modules/flag-icons/css/flag-icons.min.css'
import { PersonalizationProvider } from '@/context'

const inter = Inter({ subsets: ['latin'] })

/* https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&display=swap */
const robotoCondensed = Roboto_Condensed({
subsets: ['latin'],
weight: ['200', '300', '400', '500', '600', '700'],
style: ['normal', 'italic']
})

export const metadata: Metadata = {
title: 'Compass starter',
description: 'Provided by Contentstack'
}

/**
* @component RootLayout
* @description default layout component of the app
*
* @returns {JSX.Element}
*/
export default async function RootLayout ({
children
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode
}>) {

return (
<html lang='en'>
<body
className={`${robotoCondensed.className} ${inter.className}`}
>
<PersonalizationProvider>
<MainLayout>
{children}
</MainLayout>
</PersonalizationProvider>
</body>
</html>
<PersonalizationProvider>
<MainLayout>
{children}
</MainLayout>
</PersonalizationProvider>
)
}
10 changes: 10 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,16 @@ body {
.footer-declaimer p > a {
@apply hover:underline text-purple outline-none;
}

.user-form-consent p {
@apply font-inter text-xs leading-[125%] tracking-normal font-light;

@apply md:text-sm md:leading-[150%] ;
}

.user-form-consent p > a {
@apply underline text-white outline-none;
}
}

:root {
Expand Down
59 changes: 59 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Metadata } from 'next'
import { Inter, Roboto_Condensed } from 'next/font/google'

import 'slick-carousel/slick/slick.css'
import 'slick-carousel/slick/slick-theme.css'
import './globals.css'
import '/node_modules/flag-icons/css/flag-icons.min.css'
import { headers } from 'next/headers'
import { defaultLocale } from '@/config'

const inter = Inter({ subsets: ['latin'] })

const robotoCondensed = Roboto_Condensed({
subsets: ['latin'],
weight: ['200', '300', '400', '500', '600', '700'],
style: ['normal', 'italic']
})

export const metadata: Metadata = {
title: 'Compass starter',
description: 'Provided by Contentstack'
}

export default async function RootLayout ({
children
}: Readonly<{
children: React.ReactNode;
}>) {

const headerList = await headers()
const locale = headerList.get('x-request-locale')

return (
<html lang={locale || defaultLocale}>
<head>
<link rel='preconnect' href={process.env.CONTENTSTACK_HOST} />
<link rel='preconnect' href={process.env.CONTENTSTACK_PERSONALIZE_EDGE_API_URL} />
<link rel='preconnect' href={process.env.CONTENTSTACK_HOST?.replace(/cdn\./, 'images.')} />
<link rel='dns-fetch' href={process.env.CONTENTSTACK_API_HOST} />
<link rel='dns-fetch' href='https://contentstack.com' />
<link rel='dns-fetch' href='https://contentstack.io' />

{/* eslint-disable @next/next/google-font-preconnect */}
<link
rel='preload'
href='https://fonts.gstatic.com/s/inter/v18/UcCo3FwrK3iLTcviYwYZ8UA3.woff2'
as='font'
type='font/woff2'
crossOrigin=''
/>
</head>
<body
className={`${robotoCondensed.className} ${inter.className}`}
>
{children}
</body>
</html>
)
}
134 changes: 134 additions & 0 deletions components/CookieConsentForm/Consentform.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { CheckIcon, XMarkIcon } from '@heroicons/react/20/solid'
import React, { useEffect, useState } from 'react'
import { ConsentFormProps, LivePreviewTypeMapper } from '@/types/common'
import { getCookie, setCookie } from '@/utils'
import { Image } from '../common/Image'

/**
* Cookie Consent Form component.
*
* Displays a sticky consent popup for users to opt in or out of cookie usage.
* Handles user consent state, sets cookies, and calls optional analytics methods.
*
* @component
* @param {ConsentFormProps} props - The props for the consent form.
* @param {string} props.heading - The heading of the consent form.
* @param {string} props.content - The content shown in the consent form.
* @param {object} props.icon - Optional icon to display when consent form is minimized.
* @param {Array} props.consent_actions - List of consent actions (e.g., opt-in, opt-out).
* @param {object} [props.$] - Live preview data-cslp attributes.
* @returns {JSX.Element} The rendered consent form component.
*/
const ConsentForm:React.FC<ConsentFormProps> = ({heading, content, icon, consent_actions, $}: ConsentFormProps) => {
const [isOpen, setIsOpen] = useState(true)
const [cookieConsent, setCookieConsent] = useState('')

/**
* Handles user consent actions for cookies.
*
* Depending on the `action` parameter, this function will:
* - For `'optIn'`: Attempt to call the global `jstag.optIn()` method (if available), set the cookie consent to 'optIn', and close the consent form.
* - For `'optOut'`: Attempt to call the global `jstag.optOut()` method (if available), set the cookie consent to 'optOut', and close the consent form.
*
* Both actions update the `cookie_consent` cookie for 365 days and update the local consent state.
*
* @param action - The consent action to perform. Accepts `'optIn'` or `'optOut'`.
*/
const handleClick = (action: string) => {
if (action === 'optIn') {
try {
// @ts-ignore
if (typeof jstag !== 'undefined' && typeof jstag?.optIn === 'function') jstag?.optIn()
} catch (err) {
console.error('Error calling jstag.optIn:', err)
}
if (getCookie('cookie_consent') !== 'optIn') {
setCookie('cookie_consent', 'optIn', 365)
}
setCookieConsent('optIn')
setIsOpen(false)
} else if (action === 'optOut') {
try {
// @ts-ignore
if (typeof jstag !== 'undefined' && typeof jstag?.optOut === 'function') jstag?.optOut()
} catch (err) {
console.error('Error calling jstag.optOut:', err)
}
setCookie('cookie_consent', 'optOut', 365)
setCookieConsent('optOut')
setIsOpen(false)
}
}

useEffect(() => {
const consentCookie = getCookie('cookie_consent')
if (consentCookie === 'optOut') {
setIsOpen(false)
setCookieConsent('optOut')
try {
// @ts-ignore
if (typeof jstag !== 'undefined' && typeof jstag?.optOut === 'function') jstag?.optOut()
} catch (err) {
console.error('Error calling jstag.optOut:', err)
}
} else {
if (!consentCookie) {
setCookie('cookie_consent', 'optIn', 365)
}
setIsOpen(false)
setCookieConsent('optIn')
try {
// @ts-ignore
if (typeof jstag !== 'undefined' && typeof jstag?.optIn === 'function') jstag?.optIn()
} catch (err) {
console.error('Error calling jstag.optIn:', err)
}
}
}, [])

return(
<div className='fixed left-[30px] bottom-[30px] z-50' id='cookie-consent'>
{isOpen ? <div
{...$?.consent_modal}
className='flex max-w-lg items-center justify-center gap-2 px-4 py-2 ml-6 rounded border border-transparent bg-white font-medium
shadow-[0px_4px_15px_0px_rgba(108,92,231,0.2),0px_3px_14px_3px_rgba(0,0,0,0.12),0px_8px_10px_1px_rgba(0,0,0,0.14)] scale-100 transition-all duration-300 ease-in-out'
>
<div className='flex flex-col'>
{heading && <h6 className='mb-2 font-medium capitalize leading-tight before:hidden' {...$?.heading}>{heading}</h6>}
{content && <p className='text-sm text-gray-500' {...$?.content}>{content}</p>}
<div className='inline-flex items-center gap-2' {...$?.consent_actions}>
{consent_actions && consent_actions?.length > 0 && consent_actions.map((actionItem, index) => (
<button
key={index}
className='mt-2 text-white font-bold py-2 px-4 rounded bg-stone hover:opacity-90'
onClick={() => {
actionItem?.action && handleClick(actionItem.action)
}}
{...$?.[`consent_actions__${index}` as keyof LivePreviewTypeMapper<ConsentFormProps>] }
>
{actionItem?.label}
{cookieConsent === actionItem?.action && <CheckIcon className='h-5 w-5 text-white inline-block ml-2' aria-hidden='true' />}
</button>
))}
</div>
<XMarkIcon
className='absolute top-2 right-2 h-6 w-6 text-gray-500 cursor-pointer'
onClick={() => setIsOpen(false)}
aria-hidden='true'
/>
</div>
</div>
: <span onClick={() => setIsOpen(true)}>
{icon?.url && <Image
image={icon}
alt={heading || 'Cookie settings'}
className='w-10 h-10 hover:cursor-pointer'
/>}
</span>
}
</div>
)
}

export {ConsentForm}
1 change: 1 addition & 0 deletions components/CookieConsentForm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Consentform'
3 changes: 0 additions & 3 deletions components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ import { LanguageSelector } from '../LanguageSelector'
*/
function Header (props: App.Header): JSX.Element {

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [data, setData] = useState(props)
const { logo, items, $ } = props

const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
Expand All @@ -45,7 +43,6 @@ function Header (props: App.Header): JSX.Element {
const router = useRouterHook()

useEffect(() => {
setData(props)
if (isCookieExist(localeCookieName)) setLocales(getJsonCookie(localeCookieName))
}, [props])

Expand Down
9 changes: 8 additions & 1 deletion components/RenderComponents/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { CardCollection, FeaturedArticles, Hero, Teaser, Text, TextAndImageCarousel } from '@/components'
import dynamic from 'next/dynamic'

const CardCollection = dynamic(() => import('@/components/CardCollection').then(mod => mod.CardCollection))
const FeaturedArticles = dynamic(() => import('@/components/FeaturedArticles').then(mod => mod.FeaturedArticles))
const Hero = dynamic(() => import('@/components/Hero').then(mod => mod.Hero))
const Teaser = dynamic(() => import('@/components/Teaser').then(mod => mod.Teaser))
const Text = dynamic(() => import('@/components/Text').then(mod => mod.Text))
const TextAndImageCarousel = dynamic(() => import('@/components/TextAndImageCarousel').then(mod => mod.TextAndImageCarousel))
import { VB_EmptyBlockParentClass } from '@/config'
import { Page } from '@/types'
import { pageBlocks } from '@/types/pages'
Expand Down
2 changes: 1 addition & 1 deletion components/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ export * from './Image'
export * from './Pagination'
export * from './CTAGroup'
export * from './PageWrapper'
export * from './Video'
export * from './Video'
3 changes: 2 additions & 1 deletion components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export * from './common'
export * from './FeaturedArticles'
export * from './RenderComponents'
export * from './Carousel'
export * from './Hero'
export * from './Hero'
export * from './CookieConsentForm'
Loading