feature: intergrated directus for posts

This commit is contained in:
libertyoms
2025-04-26 19:27:16 +02:00
parent 08965c3830
commit 809f5c6ff7
11 changed files with 530 additions and 95 deletions

View File

@ -12,48 +12,37 @@ import WhyChooseUsSection, {
import FeaturedProductSection, {
defaultObseFeatures,
} from "./_components/FeaturedProductSection";
// import HeroSectionModern from "./_components/HeroSectionModern";
// import HeroSectionDynamic from "./_components/HeroSectionDynamic";
import { getHome } from "@/lib/query/home";
export default async function HomePage() {
// Explicitly type the data variable, assuming getHome returns HeroSectionType or null/undefined
const data = await getHome();
// Handle case where data might be null or undefined
if (!data) {
// Optionally return a loading state or default content
return <div>Loading hero section...</div>;
// Or render the HeroSection with default props if preferred
}
export default function HomePage() {
return (
<>
{" "}
{/*
<HeroSectionDynamic
title={<>Where Innovation Meets Excellence</>} // Simplified title for this layout
subtitle="We deliver cutting-edge IT solutions, empowering businesses to thrive in the ever-evolving digital landscape with unmatched industry expertise."
buttonText="Explore Solutions" // Changed button text slightly
buttonHref="/services" // Point to services maybe?
imageUrl="/hero-bg.jpg" // Use a different, high-quality background image
/>
<HeroSectionModern
title={
<>
Where Innovation <br className="hidden md:block" /> Meets
Excellence.
</>
}
subtitle="Welcome to Owethu Managed Services. We deliver cutting-edge IT solutions, empowering businesses to thrive in the ever-evolving digital landscape with unmatched industry expertise."
buttonText="Learn More"
buttonHref="/about"
imageUrl="/hero-bg.jpg" // Specify your hero image
/>
*/}
<HeroSection
title={
<>
Where Innovation <br className="hidden md:block" /> Meets
Excellence.
{data?.hero_title} <br className="hidden md:block" />
</>
}
subtitle="Welcome to Owethu Managed Services. We deliver cutting-edge IT solutions, empowering businesses to thrive in the ever-evolving digital landscape with unmatched industry expertise."
buttonText="Learn More"
buttonHref="/about"
imageUrl="/hero-bg.jpg" // Specify your hero image
subtitle={
data.hero_subtitle || "Your trusted partner in technology solutions."
} // Use optional chaining and provide a default
buttonText={data.hero_buttons?.[0]?.label || "Learn More"} // Use optional chaining and provide a default
buttonHref={data.hero_buttons?.[0]?.link || "/about"} // Use optional chaining and provide a default
imageUrl={
data.hero_cover
? `${process.env.DIRECTUS_API_ENDPOINT}/assets/${data.hero_cover}`
: "/hero-bg.jpg"
} // Use optional chaining and provide a default
/>
<CoreServicesSection
title="Core Services"

View File

@ -0,0 +1,294 @@
import { notFound } from "next/navigation";
import Image from "next/image";
import { getPostBySlug, getPosts } from "@/lib/query/post";
import type { Metadata } from "next";
import { format } from "date-fns";
import {
ContentBlock,
OutputData,
ParagraphData,
HeaderData,
ListData,
ImageData,
QuoteData,
CodeData,
} from "@/types";
type Props = {
params: { slug: string };
};
export async function generateStaticParams() {
const posts = await getPosts();
if (!Array.isArray(posts)) {
console.error("getPosts did not return an array:", posts);
return [];
}
return posts.map((post) => ({
slug: post.slug,
}));
}
function extractDescriptionFromBlocks(
content: OutputData | null
): string | null {
if (
!content ||
!content.blocks ||
!Array.isArray(content.blocks) ||
content.blocks.length === 0
) {
return null;
}
const firstParagraph = content.blocks.find(
(block): block is ContentBlock & { data: ParagraphData } =>
block.type === "paragraph"
);
if (firstParagraph?.data?.text) {
const plainText = firstParagraph.data.text
.replace(/<[^>]*>/g, "")
.replace(/&[^;]+;/g, "");
return plainText.substring(0, 160) + (plainText.length > 160 ? "..." : "");
}
return null;
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug, {
fields: [
"slug",
"status",
"user_created",
"date_created",
"user_updated",
"date_updated",
"title",
"content",
"excerpt",
"featured_image",
],
});
if (!post) {
return {
title: "Post Not Found",
};
}
const imageUrl = post.featured_image;
const descriptionFromContent =
post.content &&
typeof post.content === "object" &&
Array.isArray(post.content.blocks)
? extractDescriptionFromBlocks(post.content as OutputData)
: null;
const description =
post.excerpt || descriptionFromContent || "Read this OMS TechTalk article.";
const publishedTime = post.date_created
? new Date(post.date_created).toISOString()
: "";
return {
title: `${post.title} | OMS TechTalk`,
description: description,
alternates: {
canonical: `/tech-talk/${post.slug}`,
},
openGraph: {
title: post.title,
description: description,
url: `https://oms.africa/tech-talk/${post.slug}`,
type: "article",
publishedTime: publishedTime,
...(imageUrl && {
images: [
{
url: imageUrl,
width: 1200,
height: 630,
alt: post.title,
},
],
}),
},
twitter: {
card: "summary_large_image",
title: post.title,
description: description,
...(imageUrl && { images: [imageUrl] }),
},
};
}
const renderBlock = (block: ContentBlock) => {
const renderListItems = (items: string[]) => {
return items.map((item, index) => (
<li
key={index}
className="mb-2"
dangerouslySetInnerHTML={{ __html: item }}
/>
));
};
switch (block.type) {
case "header":
const headerBlock = block as ContentBlock & { data: HeaderData };
const level = headerBlock.data.level || 2;
// Ensure HeaderTag has a type that JSX understands for intrinsic elements.
const HeaderTag = `h${level}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
return (
<HeaderTag
key={block.id}
className={`text-${
6 - (headerBlock.data.level || 2)
}xl font-bold my-4 text-primary`}
dangerouslySetInnerHTML={{ __html: headerBlock.data.text }}
/>
);
case "paragraph":
const paragraphBlock = block as ContentBlock & { data: ParagraphData };
return (
<p
key={block.id}
className="my-4 leading-relaxed"
dangerouslySetInnerHTML={{ __html: paragraphBlock.data.text }}
/>
);
case "list":
const listBlock = block as ContentBlock & { data: ListData };
const ListTag = listBlock.data.style === "ordered" ? "ol" : "ul";
return (
<ListTag
key={block.id}
className={`my-4 pl-6 ${
listBlock.data.style === "ordered" ? "list-decimal" : "list-disc"
}`}
>
{listBlock.data.items &&
Array.isArray(listBlock.data.items) &&
renderListItems(listBlock.data.items)}
</ListTag>
);
case "image":
const imageBlock = block as ContentBlock & { data: ImageData };
const imageUrl = imageBlock.data.file?.url;
if (!imageUrl) return null;
return (
<div key={block.id} className="my-6">
<Image
src={imageUrl}
alt={imageBlock.data.caption || "Blog post image"}
width={imageBlock.data.file?.width || 800}
height={imageBlock.data.file?.height || 450}
className="rounded-md shadow-sm mx-auto"
/>
{imageBlock.data.caption && (
<p className="text-center text-sm text-muted-foreground mt-2">
{imageBlock.data.caption}
</p>
)}
</div>
);
case "quote":
const quoteBlock = block as ContentBlock & { data: QuoteData };
return (
<blockquote
key={block.id}
className="my-6 pl-4 border-l-4 border-primary italic text-muted-foreground"
>
<p dangerouslySetInnerHTML={{ __html: quoteBlock.data.text }} />
{quoteBlock.data.caption && (
<footer className="text-sm mt-2">
- {quoteBlock.data.caption}
</footer>
)}
</blockquote>
);
case "code":
const codeBlock = block as ContentBlock & { data: CodeData };
const codeContent = codeBlock.data.code || "";
return (
<pre
key={block.id}
className="my-6 p-4 bg-muted text-muted-foreground rounded-md overflow-x-auto text-sm"
>
<code>{codeContent}</code>
</pre>
);
default:
console.warn(`Unknown block type: ${block.type}`);
return null;
}
};
export default async function PostPage({ params }: Props) {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) {
notFound();
}
const contentBlocks =
post.content &&
typeof post.content === "object" &&
Array.isArray(post.content.blocks)
? (post.content.blocks as ContentBlock[])
: null;
const coverImageUrl =
post.featured_image + "?width=1200&height=600&quality=80&fit=cover";
return (
<article className="container mx-auto px-4 py-8">
<header className="text-center mb-12 max-w-3xl mx-auto">
{coverImageUrl && (
<div className="relative w-full h-64 md:h-96 mb-6 rounded-lg overflow-hidden">
<Image
src={coverImageUrl}
alt={`Cover image for ${post.title}`}
fill={true}
style={{ objectFit: "cover" }}
className="shadow-md"
priority
/>
</div>
)}
<h1 className="text-4xl md:text-5xl font-extrabold leading-tight mb-4 text-primary">
{post.title}
</h1>
{post.date_created && (
<p className="text-muted-foreground text-sm mb-2">
Published on {format(new Date(post.date_created), "MMMM dd, yyyy")}
</p>
)}
{post.excerpt && (
<p className="text-lg text-muted-foreground italic mt-4">
{post.excerpt}
</p>
)}
</header>
<div className="container mx-auto px-4 max-w-3xl">
<div
className="prose prose-lg lg:prose-xl dark:prose-invert max-w-none mx-auto
prose-headings:text-primary prose-a:text-blue-600 hover:prose-a:text-blue-800 dark:prose-a:text-blue-400 dark:hover:prose-a:text-blue-300
prose-strong:font-semibold prose-img:rounded-md prose-img:shadow-sm"
>
{contentBlocks && contentBlocks.length > 0 ? (
contentBlocks.map((block) => renderBlock(block))
) : (
<p>Content is not available or in an unexpected format.</p>
)}
</div>
</div>
</article>
);
}

View File

@ -1,39 +1,8 @@
import React from "react";
import BlogPostCard from "@/components/BlogPostCard";
import { prisma } from "@/lib/prisma"; // Import Prisma client
import { auth } from "@/auth"; // Import auth to check session
import Button from "@/components/ui/Button"; // Import Button component
import type { Metadata } from "next";
interface Post {
id: string;
slug: string;
title: string;
content: string;
excerpt?: string | null;
imageUrl?: string | null;
published: boolean;
authorId: string;
createdAt: Date;
updatedAt: Date;
tags?: string[];
}
// --- Fetch Posts ---
async function getPublishedPosts() {
try {
const posts = await prisma.post.findMany({
where: { published: true },
orderBy: { createdAt: "desc" },
// select needed fields if not all
});
return posts;
} catch (error) {
console.error("Failed to fetch posts:", error);
return []; // Return empty array on error
}
}
import { getPosts } from "@/lib/query/post";
import { Post } from "@/types";
// --- SEO Metadata ---
export const metadata: Metadata = {
title: "OMS TechTalk | Insights & Innovation",
@ -65,8 +34,7 @@ export const metadata: Metadata = {
// --- Page Component ---
const TechTalkPage = async () => {
const posts = await getPublishedPosts();
const session = await auth(); // Get session info
const posts: Post[] = await getPosts();
return (
<div className="bg-background text-foreground">
@ -80,28 +48,21 @@ const TechTalkPage = async () => {
Insights, trends, and discussions on the latest in technology,
innovation, and digital transformation from the experts at OMS.
</p>
{/* Conditionally render Create Post button */}
{session?.user && (
<div className="mt-8">
<Button href="/tech-talk/create" variant="primary">
Create New Post
</Button>
</div>
)}
</div>
{/* Blog Post Grid */}
{posts.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-10">
{posts.map((post: Post) => (
<BlogPostCard
key={post.id} // Use post ID as key
key={post.slug}
slug={post.slug}
title={post.title}
excerpt={post.excerpt ?? post.content.substring(0, 150) + "..."}
// Use imageUrl from DB or a default placeholder
imageUrl={post.imageUrl ?? "/posts/default-placeholder.jpg"} // Provide a default image
author={"OMS Team"} // Replace with actual author logic if available (e.g., post.user.name)
date={new Date(post.createdAt).toLocaleDateString("en-US", {
excerpt={post.excerpt ?? "No excerpt available"}
imageUrl={
post.featured_image ?? "/posts/default-placeholder.jpg"
}
author={"OMS Team"}
date={new Date(post.date_created).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",