CookieConsent.tsx
import Link from 'next/link'
import Cookies from 'js-cookie'
import { MouseEvent, useCallback, useEffect, useState } from 'react'
import { Container } from '../layouts/PublicLayout'
const USER_CONSENT_COOKIE_KEY = 'cookie_consent_is_true'
const USER_CONSENT_COOKIE_EXPIRE_DATE =
new Date().getTime() + 365 * 24 * 60 * 60
const CookieConsent = () => {
const [cookieConsentIsTrue, setCookieConsentIsTrue] = useState(true)
useEffect(() => {
const consentIsTrue = Cookies.get(USER_CONSENT_COOKIE_KEY) === 'true'
setCookieConsentIsTrue(consentIsTrue)
}, [])
const onClick = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
if (!cookieConsentIsTrue) {
Cookies.set(USER_CONSENT_COOKIE_KEY, 'true', {
expires: USER_CONSENT_COOKIE_EXPIRE_DATE,
})
setCookieConsentIsTrue(true)
}
}
if (cookieConsentIsTrue) {
return null
}
return (
<section className="fixed bottom-0 left-0 w-full py-2 md:py-4">
<Container>
<div className="flex flex-col items-start px-5 py-3 space-y-2 bg-gray-200 md:flex-row md:space-y-0 md:items-stretch md:space-x-2">
<div className="flex items-center flex-grow text-gray-900">
<p className="text-sm font-medium">
This site uses services that uses cookies to deliver better
experience and analyze traffic. You can learn more about the
services we use at our{' '}
<Link href="/privacy-policy">
<a className="text-sm underline hover:text-lightAccent">
privacy policy
</a>
</Link>
.
</p>
</div>
<div className="flex items-center">
<button
className="p-3 text-sm font-bold text-white uppercase bg-gray-700 whitespace-nowrap"
onClick={onClick}
>
Got it
</button>
</div>
</div>
</Container>
</section>
)
}
export default CookieConsent
PublicLayout.tsx
import Link from 'next/link'
import { ReactNode } from 'react'
import CookieConsent from '../banners/CookieConsent'
export const Container = ({ children }: { children: ReactNode }) => (
<div className="w-full max-w-5xl px-2 mx-auto md:px-4">{children}</div>
)
const PublicLayout = ({ children }: { children: ReactNode }) => {
return (
<div className="flex flex-col min-h-screen">
<header className="flex items-center border-b border-gray-200 h-14">
<Container>
<nav>
<ul>
<li>
<Link href="/">
<a className="text-sm font-medium text-center capitalize">
Home
</a>
</Link>
</li>
</ul>
</nav>
</Container>
</header>
<main className="flex-grow">
<Container>{children}</Container>
</main>
<footer className="py-5 border-t border-gray-200">
<Container>
<nav className="my-5">
<ul>
<li>
<Link href="/privacy-policy">
<a className="text-sm font-medium text-center capitalize">
Privacy Policy
</a>
</Link>
</li>
</ul>
</nav>
</Container>
<Container>
<p className="text-sm font-medium text-center capitalize">
copyright © {new Date().getFullYear()} all rights reserved
</p>
</Container>
</footer>
<CookieConsent />
</div>
)
}
export default PublicLayout
hello.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' })
}
_app.tsx
import '../styles/globals.scss'
import type { AppProps } from 'next/app'
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
export default MyApp
_document.tsx
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
} from "next/document";
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const originalRenderPage = ctx.renderPage;
// Run the React rendering logic synchronously
ctx.renderPage = async () => {
return originalRenderPage({
// Useful for wrapping the whole react tree
enhanceApp: (App) =>
function enhanceApp(props) {
return <App {...props} />;
},
// Useful for wrapping in a per-page basis
enhanceComponent: (Component) => Component,
});
};
// Run the parent `getInitialProps`, it now includes the custom `renderPage`
const initialProps = await Document.getInitialProps(ctx);
return initialProps;
}
render() {
return (
<Html>
<Head>
<meta charSet="utf-8" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
index.tsx
import Head from 'next/head'
import type { NextPage } from 'next'
import PublicLayout from '../components/layouts/PublicLayout'
const Home: NextPage = () => {
return (
<>
<Head>
<title>Next.js cookie consent banner</title>
<meta
name="description"
content="A Next.js cookie consent banner with TypeScript and Tailwind CSS."
/>
</Head>
<PublicLayout>
<h1 className="text-3xl font-bold font-open">
Next.js cookie consent banner
</h1>
</PublicLayout>
</>
)
}
export default Home
privacy-policy.tsx
import Head from 'next/head'
import type { NextPage } from 'next'
import PublicLayout from '../components/layouts/PublicLayout'
const PrivacyPolicy: NextPage = () => {
return (
<>
<Head>
<title>Our Privacy Policy</title>
<meta name="description" content="Website privacy policy page" />
</Head>
<PublicLayout>
<h1 className="text-3xl font-bold font-open">
Website privacy policy page content
</h1>
</PublicLayout>
</>
)
}
export default PrivacyPolicy
globals.scss
@tailwind base;
@tailwind components;
@tailwind utilities;.eslintrc.json
{
"extends": "next/core-web-vitals"
}
.prettierrc
{
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all",
"semi": false
}
next-env.d.ts
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig
package.json
{
"name": "nextjs_cookie_consent_banner",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"js-cookie": "^3.0.1",
"next": "12.2.3",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@types/js-cookie": "^3.0.2",
"@types/node": "18.0.6",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"autoprefixer": "^10.4.7",
"eslint": "8.20.0",
"eslint-config-next": "12.2.3",
"postcss": "^8.4.14",
"sass": "^1.54.0",
"tailwindcss": "^3.1.6",
"typescript": "4.7.4"
}
}postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};
tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}