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
10 changes: 2 additions & 8 deletions src/app/(frontend)/blogs/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@ import Header from '@/components/common/Header'
import Footer from '@/components/common/Footer'
import { notFound } from 'next/navigation'
import { getBlogBySlug } from '@/lib/payload/blogs'
import Template1 from '@/components/blogs/templates/Template1'
import Template2 from '@/components/blogs/templates/Template2'

const templates = {
template1: Template1,
template2: Template2,
} as const
import { BLOG_TEMPLATES } from '@/lib/blog-templates'

export default async function BlogPage({
params,
Expand All @@ -20,7 +14,7 @@ export default async function BlogPage({
const blog = await getBlogBySlug(slug)
if (!blog) return notFound()

const Template = templates[blog.template]
const Template = BLOG_TEMPLATES[blog.template]

return (
<>
Expand Down
5 changes: 5 additions & 0 deletions src/assets/groupedLeaves.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions src/collections/Blogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ export const Blogs: CollectionConfig = {
beforeValidate: [
async ({ data = {}, originalDoc, req, operation }) => {
const baseSource =
data.slug ||
data.title ||
(operation === 'update' ? originalDoc?.title : '')
data.slug || data.title || (operation === 'update' ? originalDoc?.title : '')

const base = slugify((baseSource || '').toString(), {
lower: true,
Expand Down Expand Up @@ -103,6 +101,8 @@ export const Blogs: CollectionConfig = {
options: [
{ label: 'Template 1', value: 'template1' },
{ label: 'Template 2', value: 'template2' },
{ label: 'Template 3', value: 'template3' },
{ label: 'Template 4', value: 'template4' },
],
admin: {
description: 'Select which layout template to use when rendering this blog.',
Expand Down
132 changes: 132 additions & 0 deletions src/components/blogs/templates/Template3.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React from 'react'
import Image from 'next/image'
import gL from '@/assets/groupedLeaves.svg'
import authorImage from '@/assets/people_placeholder.png'
import type { BlogType } from '@/types/blog'

interface Template3Props {
blog: BlogType
}

export default function Template3({ blog }: Template3Props) {
return (
<div className="blog-detail overflow-x-hidden">
{/* Navigation section */}
<nav className="text-xs sm:text-sm text-black mt-4 mb-2 pl-[2rem] relative">
<div className="md:flex md:justify-between md:items-start">
<div>
BLOGS {'>'}{' '}
{/* TODO: Remove placeholder fields once the field is added to Payload blog collection */}
<span className="uppercase">{
// blog.category ||
'Behind the Scenes'}</span>:{' '}
<span className="text-black">{blog.title}</span>
</div>
</div>
</nav>

{/* Hero block */}
<div className="w-[100vw] md:w-[70vw] aspect-[16/9] relative md:rounded-xl overflow-hidden my-10 mx-auto">
<Image
src={blog.imageUrl}
alt={blog.imageAlt}
className="object-cover"
fill
/>

<div className="absolute inset-0 bg-black/40 flex flex-col justify-end p-6">
<p className="uppercase tracking-widest text-[0.8rem] text-white/60">
{/* TODO: Remove placeholder fields once the field is added to Payload blog collection */}
{
// blog.category ||
'Behind the Scenes'}
</p>
<h1 className="mt-2 text-[clamp(2rem,4vw,4rem)] font-bold leading-tight text-white">
{blog.title}
</h1>
<p className="text-gray-200">{blog.description}</p>
<div className="text-xs text-white mt-1">
<span className="text-white/60">Written By: </span>
<span className="font-semibold">{blog.authorName}</span>
</div>
</div>
</div>

<article className="relative mt-[2rem] pb-[5rem] max-w-6xl mx-auto">
{/* Content + Sidebar Image */}
<div className="mt-[8vh] grid grid-cols-1 md:grid-cols-2 gap-[15vw] md:gap-[5vw] px-[9vw]">
<div className="text-[0.8125rem] leading-[1.75rem] text-black">
<div
dangerouslySetInnerHTML={{
__html:
blog.content?.root?.children
?.map((child: any) =>
child.type === 'paragraph'
? `<p>${(child.children ?? [])
.map((c: any) => c.text || '')
.join('')}</p>`
: '',
)
.join('') || 'Content coming soon...',
}}
/>
</div>

<div className="relative flex flex-col items-end w-full">
<div className="sticky top-6 w-[95%] aspect-[3/4]">
<Image
src={blog.imageUrl}
alt={blog.imageAlt}
className="object-cover rounded-xl overflow-hidden border border-gray-300"
fill
/>
<div className="absolute -bottom-6 -left-6 bg-[#13384E] text-white p-4 rounded-md shadow-lg max-w-[60%] text-center">
<p className="text-sm md:text-base font-semibold">
{/* TODO: Remove placeholder fields once the field is added to Payload blog collection */}
{
// blog.quote ||
`"Insert an inspirational quote here!"`}
</p>
{/* TODO: Remove placeholder fields once the field is added to Payload blog collection */}
<p className="text-xs mt-2">{
// blog.quoteAuthor ||
'- Author'}</p>
</div>
</div>
</div>
</div>

{/* About the Author */}
<section className="my-[15vh] md:mt-[10vh] px-[9vw]">
<div className="grid grid-cols-1 md:grid-cols-3 items-center gap-[2rem] max-w-6xl mx-auto">
<div className="flex justify-center">
<div className="relative w-[45%] aspect-square rounded-full overflow-hidden border border-gray-300 shadow-md">
<Image
src={authorImage}
alt={blog.authorName || 'Author'}
fill
className="object-cover"
/>
</div>
</div>

<div className="md:col-span-2 text-center md:text-left space-y-3">
<h2 className="text-2xl font-bold text-[#13384E]">About the Author</h2>
<p className="text-sm text-gray-700 leading-relaxed">
{/* TODO: Remove placeholder fields once the field is added to Payload blog collection */}
{
// blog.authorBio ||
'This is a short bio about the author. They share stories, insights, and moments that inspire others.'}
</p>
</div>
</div>
</section>

{/* Bottom Decoration */}
<div className="flex justify-center">
<Image src={gL} alt="Decorative leaves" className="w-[50vw]" />
</div>
</article>
</div>
)
}
187 changes: 187 additions & 0 deletions src/components/blogs/templates/Template4.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import React from 'react'
import Image from 'next/image'
import bigGreenKoru from '@/assets/bigGreenKoru.png'
import leaf from '@/assets/leaf.svg'
import type { BlogType } from '@/types/blog'

interface Template3Props {
blog: BlogType
}

export default function Template4({ blog }: Template3Props) {
// Split content into different sections
const getContentSections = () => {
const paragraphs =
blog.content?.root?.children
?.filter((child: any) => child.type === 'paragraph')
?.map((child: any) => (child.children ?? []).map((c: any) => c.text || '').join('')) || []

const totalParagraphs = paragraphs.length
const sectionSize = Math.ceil(totalParagraphs / 3)

return {
section1: paragraphs.slice(0, sectionSize),
section2: paragraphs.slice(sectionSize, sectionSize * 2),
section3: paragraphs.slice(sectionSize * 2),
}
}

const contentSections = getContentSections()

const renderContent = (paragraphs: string[]) => {
if (paragraphs.length === 0) return 'Content coming soon...'
return paragraphs.map((text, index) => `<p>${text}</p>`).join('')
}

return (
<div className="blog-detail overflow-x-hidden">
{/* Breadcrumb Navigation */}
<nav className="text-xs sm:text-sm text-black mt-4 mb-2 pl-[2rem] relative">
<div className="md:flex md:justify-between md:items-start">
<div>
BLOGS {'>'} <span className="uppercase">Stories</span>:{' '}
<span className="text-black">{blog.title}</span>
</div>

<div className="text-xs text-black mt-1 md:mt-0 md:absolute md:right-[2rem] md:top-0">
<span className="text-gray-600">Written By: </span>
<span className="font-semibold">{blog.authorName}</span>
</div>
</div>
</nav>

{/* Hero Section */}
<article className="relative pt-[1.5rem] pb-[5rem] max-w-6xl mx-auto">
<p className="uppercase tracking-widest text-[0.8rem] text-black text-center">Stories</p>

<h1 className="mt-2 text-center text-[clamp(1.5rem,4vw,4rem)] font-bold leading-tight text-black relative z-10">
{blog.title}
</h1>

{/* Large decorative koru background */}
<div className="hidden lg:block absolute -left-[clamp(1.5rem,11vw,18rem)] top-[clamp(15rem,40vh,25rem)] z-0 w-[clamp(10rem,18vw,16rem)] opacity-70">
<Image src={bigGreenKoru} alt="Kiwi decoration" className="rotate-[70deg]" />
</div>

{blog.description && (
<div className="mt-[1.25rem] text-center text-[0.6875rem] tracking-wider text-black">
{blog.description}
</div>
)}

{/* Main Content Section */}
<div className="mt-[3rem] px-[2rem] md:px-[4rem] lg:px-[6rem]">
<div className="max-w-5xl mx-auto">
{/* First Row - Image Left, Text Right */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-[4rem] items-start mb-[4rem]">
<div className="relative">
<div className="aspect-[4/3] relative rounded-xl overflow-hidden border border-gray-300">
<Image src={blog.imageUrl} alt={blog.title} className="object-cover" fill />
</div>
{/* Decorative leaves */}
<div className="absolute -top-[1rem] -left-[1rem] w-[clamp(3rem,8vw,4rem)] h-[clamp(3rem,8vw,4rem)] z-10">
<Image src={leaf} alt="Decorative leaf" className="w-full h-full" />
</div>
<div className="absolute -bottom-[1rem] -right-[1rem] w-[clamp(3rem,8vw,4rem)] h-[clamp(3rem,8vw,4rem)] z-10">
<Image
src={leaf}
alt="Decorative leaf"
className="w-full h-full scale-x-[-1] rotate-6"
/>
</div>
</div>

<div className="space-y-[1.25rem] text-[0.8125rem] leading-[1.75rem] text-black">
<div
dangerouslySetInnerHTML={{
__html: renderContent(contentSections.section1),
}}
/>
</div>
</div>

{/* Second Row - Text Left, Image Right */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-[4rem] items-start mb-[4rem]">
<div className="space-y-[1.25rem] text-[0.8125rem] leading-[1.75rem] text-black">
<div
dangerouslySetInnerHTML={{
__html: renderContent(contentSections.section2),
}}
/>
</div>

<div className="relative">
<div className="aspect-[4/3] relative rounded-xl overflow-hidden border border-gray-300">
<Image src={blog.imageUrl} alt={blog.title} className="object-cover" fill />
</div>
{/* Blue koru decoration */}
<div className="absolute -bottom-[1.5rem] -left-[1.5rem] w-[clamp(4rem,10vw,6rem)] h-[clamp(4rem,10vw,6rem)] z-10">
<Image
src={bigGreenKoru}
alt="Decorative koru"
className="w-full h-full scale-x-[-1] rotate-12 opacity-80"
/>
</div>
</div>
</div>

{/* Quote Section */}
<div className="text-center my-[4rem]">
{/* TODO: Remove the placeholder quote once the field is added to Payload blog collection */}
<blockquote className="text-[clamp(1.2rem,2.5vw,1.8rem)] italic font-light text-black leading-relaxed mb-4">
"it doloribus ut rerum culpa est eligendi veniam Aut quia en assumenda eum pa
nostrum vel"
</blockquote>
<footer className="text-sm text-gray-600 font-medium">- Pauline Smith</footer>
</div>

{/* Third Row - Image Left, Text Right */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-[4rem] items-start">
<div className="relative">
<div className="aspect-[4/3] relative rounded-xl overflow-hidden border border-gray-300">
<Image src={blog.imageUrl} alt={blog.title} className="object-cover" fill />
</div>
{/* Decorative leaves */}
<div className="absolute -top-[1rem] -left-[1rem] w-[clamp(3rem,8vw,4rem)] h-[clamp(3rem,8vw,4rem)] z-10">
<Image src={leaf} alt="Decorative leaf" className="w-full h-full" />
</div>
<div className="absolute -bottom-[1rem] -right-[1rem] w-[clamp(3rem,8vw,4rem)] h-[clamp(3rem,8vw,4rem)] z-10">
<Image
src={leaf}
alt="Decorative leaf"
className="w-full h-full scale-x-[-1] rotate-6"
/>
</div>
</div>

<div className="space-y-[1.25rem] text-[0.8125rem] leading-[1.75rem] text-black">
<div
dangerouslySetInnerHTML={{
__html: renderContent(contentSections.section3),
}}
/>
</div>
</div>

{/* Bottom decorative leaves */}
<div className="flex justify-center gap-[2rem] mt-[4rem]">
<div className="w-[clamp(2rem,6vw,3rem)] h-[clamp(2rem,6vw,3rem)]">
<Image src={leaf} alt="Decorative leaf" className="w-full h-full" />
</div>
<div className="w-[clamp(2rem,6vw,3rem)] h-[clamp(2rem,6vw,3rem)]">
<Image
src={leaf}
alt="Decorative leaf"
className="w-full h-full scale-x-[-1] rotate-12"
/>
</div>
<div className="w-[clamp(2rem,6vw,3rem)] h-[clamp(2rem,6vw,3rem)]">
<Image src={leaf} alt="Decorative leaf" className="w-full h-full rotate-6" />
</div>
</div>
</div>
</div>
</article>
</div>
)
}
16 changes: 16 additions & 0 deletions src/lib/blog-templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Template1 from '@/components/blogs/templates/Template1'
import Template2 from '@/components/blogs/templates/Template2'
import Template3 from '@/components/blogs/templates/Template3'
import Template4 from "@/components/blogs/templates/Template4";

// All valid template keys
export const TEMPLATE_KEYS = ['template1', 'template2', 'template3', 'template4'] as const
export type TemplateKey = typeof TEMPLATE_KEYS[number]

// Map of key -> actual React component
export const BLOG_TEMPLATES: Record<TemplateKey, React.FC<any>> = {
template1: Template1,
template2: Template2,
template3: Template3,
template4: Template4,
}
3 changes: 2 additions & 1 deletion src/lib/mapBlog.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Blog } from '@/payload-types'
import placeholderImage from '@/assets/landscape_placeholder.png'
import { TEMPLATE_KEYS, type TemplateKey, type BlogType } from '@/types/blog'
import { type BlogType } from '@/types/blog'
import { TEMPLATE_KEYS, type TemplateKey } from "@/lib/blog-templates";

export function mapPayloadBlog(b: Blog): BlogType {
const template: TemplateKey = TEMPLATE_KEYS.includes(b.template as TemplateKey)
Expand Down
Loading