diff --git a/app/(website)/page.tsx b/app/(website)/page.tsx index 4b376d1..b1d21df 100644 --- a/app/(website)/page.tsx +++ b/app/(website)/page.tsx @@ -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
Loading hero section...
; + // Or render the HeroSection with default props if preferred + } -export default function HomePage() { return ( <> - {" "} - {/* - 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 - /> - - - - - Where Innovation
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 - /> -*/} - Where Innovation
Meets - Excellence. + {data?.hero_title}
} - 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 /> ({ + 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 { + 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) => ( +
  • + )); + }; + + 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 ( + + ); + case "paragraph": + const paragraphBlock = block as ContentBlock & { data: ParagraphData }; + return ( +

    + ); + case "list": + const listBlock = block as ContentBlock & { data: ListData }; + const ListTag = listBlock.data.style === "ordered" ? "ol" : "ul"; + return ( + + {listBlock.data.items && + Array.isArray(listBlock.data.items) && + renderListItems(listBlock.data.items)} + + ); + case "image": + const imageBlock = block as ContentBlock & { data: ImageData }; + const imageUrl = imageBlock.data.file?.url; + + if (!imageUrl) return null; + + return ( +

    + {imageBlock.data.caption + {imageBlock.data.caption && ( +

    + {imageBlock.data.caption} +

    + )} +
    + ); + case "quote": + const quoteBlock = block as ContentBlock & { data: QuoteData }; + return ( +
    +

    + {quoteBlock.data.caption && ( +

    + - {quoteBlock.data.caption} +
    + )} +
    + ); + case "code": + const codeBlock = block as ContentBlock & { data: CodeData }; + const codeContent = codeBlock.data.code || ""; + return ( +
    +          {codeContent}
    +        
    + ); + 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 ( +
    +
    + {coverImageUrl && ( +
    + {`Cover +
    + )} +

    + {post.title} +

    + {post.date_created && ( +

    + Published on {format(new Date(post.date_created), "MMMM dd, yyyy")} +

    + )} + {post.excerpt && ( +

    + {post.excerpt} +

    + )} +
    + +
    +
    + {contentBlocks && contentBlocks.length > 0 ? ( + contentBlocks.map((block) => renderBlock(block)) + ) : ( +

    Content is not available or in an unexpected format.

    + )} +
    +
    +
    + ); +} diff --git a/app/(website)/tech-talk/page.tsx b/app/(website)/tech-talk/page.tsx index 2cef120..4b874d0 100644 --- a/app/(website)/tech-talk/page.tsx +++ b/app/(website)/tech-talk/page.tsx @@ -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 (
    @@ -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.

    - {/* Conditionally render Create Post button */} - {session?.user && ( -
    - -
    - )}
    {/* Blog Post Grid */} {posts.length > 0 ? (
    {posts.map((post: Post) => ( = ({ author, date, }) => { - console.log("BlogPostCard Props:", { imageUrl }); - return (
    {/* Image Container */} -
    - {title} - {/* Optional: Subtle overlay on hover */} -
    +
    + {imageUrl ? ( + {title} + ) : ( + // Optional: Placeholder if no image +
    + No Image +
    + )}
    {/* Content Area */} diff --git a/lib/directus.ts b/lib/directus.ts new file mode 100644 index 0000000..aec569b --- /dev/null +++ b/lib/directus.ts @@ -0,0 +1,5 @@ +import { createDirectus, rest } from "@directus/sdk"; + +export const directus = createDirectus( + String(process.env.DIRECTUS_API_ENDPOINT!) +).with(rest()); diff --git a/lib/query/home.ts b/lib/query/home.ts new file mode 100644 index 0000000..9d3880c --- /dev/null +++ b/lib/query/home.ts @@ -0,0 +1,8 @@ +import { readItems } from "@directus/sdk"; +import { directus } from "../directus"; +import { HeroSection } from "@/types"; + +export async function getHome() { + // Assuming '1' is the primary key for the singleton "home" item + return directus.request(readItems("home")) as unknown as HeroSection; +} diff --git a/lib/query/post.ts b/lib/query/post.ts new file mode 100644 index 0000000..47b540a --- /dev/null +++ b/lib/query/post.ts @@ -0,0 +1,66 @@ +import { directus } from "@/lib/directus"; +import { ItemsQuery, Post } from "@/types"; +import { readItem, readItems } from "@directus/sdk"; + +// Construct base URL for assets from environment variable +const assetsUrl = `${process.env.DIRECTUS_API_ENDPOINT}/assets/`; + +function getFullImageUrl(imageId: string | null | undefined): string | null { + if (!imageId || !assetsUrl) { + return null; + } + return `${assetsUrl}${imageId}`; +} + +// Function to fetch all published posts +export async function getPosts(): Promise { + try { + const postsData = await directus.request( + readItems("posts", { + fields: [ + "slug", + "title", + "excerpt", + "featured_image", + "date_created", + "content", + ], + filter: { + status: { _eq: "published" }, + }, + sort: ["-date_created"], + }) + ); + + const posts = postsData.map((post) => ({ + ...post, + featured_image: getFullImageUrl(post.featured_image), + })) as Post[]; + + return posts; + } catch (error) { + console.error("Error fetching posts:", error); + return []; + } +} + +// Function to fetch a single post by slug +export async function getPostBySlug( + slug: string, + options?: ItemsQuery +): Promise { + try { + const postData = await directus.request(readItem("posts", slug, options)); + + // Map data to include full image URL + const post = { + ...postData, + featured_image: getFullImageUrl(postData.featured_image), + } as Post; // Adjust cast if needed + + return post; + } catch (error) { + console.error(`Error fetching post with slug ${slug}:`, error); + return null; + } +} diff --git a/next.config.ts b/next.config.ts index 05c512b..a8c61cf 100644 --- a/next.config.ts +++ b/next.config.ts @@ -11,6 +11,10 @@ const nextConfig: NextConfig = { protocol: "https", hostname: "storage.cvevolve.com", }, + { + protocol: "https", + hostname: process.env.DIRECTUS_API_ENDPOINT!.replace("https://", ""), + }, ], }, experimental: { diff --git a/package-lock.json b/package-lock.json index f87c7d5..e6d5b84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,10 @@ "version": "0.1.0", "dependencies": { "@auth/prisma-adapter": "^2.9.0", + "@directus/sdk": "^18.0.3", "@google/generative-ai": "^0.24.0", "@prisma/client": "^6.6.0", + "date-fns": "^4.1.0", "minio": "^8.0.5", "next": "15.3.1", "next-auth": "^5.0.0-beta.26", @@ -646,6 +648,18 @@ "node": ">=6.9.0" } }, + "node_modules/@directus/sdk": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@directus/sdk/-/sdk-18.0.3.tgz", + "integrity": "sha512-PnEDRDqr2x/DG3HZ3qxU7nFp2nW6zqJqswjii57NhriXgTz4TBUI8NmSdzQvnyHuTL9J0nedYfQGfW4v8odS1A==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/directus/directus?sponsor=1" + } + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -4604,6 +4618,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", diff --git a/package.json b/package.json index e8e55bc..795a5e2 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ }, "dependencies": { "@auth/prisma-adapter": "^2.9.0", + "@directus/sdk": "^18.0.3", "@google/generative-ai": "^0.24.0", "@prisma/client": "^6.6.0", + "date-fns": "^4.1.0", "minio": "^8.0.5", "next": "15.3.1", "next-auth": "^5.0.0-beta.26", diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..34b3d16 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,79 @@ +export interface OutputData { + time: number; + blocks: ContentBlock[]; + version: string; +} + +export interface ContentBlock { + id: string; + type: string; + data: + | ParagraphData + | HeaderData + | ListData + | ImageData + | QuoteData + | CodeData; +} + +export interface ParagraphData { + text: string; +} + +export interface HeaderData { + text: string; + level: number; +} + +export interface ListData { + style: "ordered" | "unordered"; + items: string[]; +} + +export interface ImageData { + file: { + url: string; + width?: number; + height?: number; + }; + caption?: string; +} + +export interface QuoteData { + text: string; + caption?: string; +} + +export interface CodeData { + code: string; +} + +export interface Post { + slug: string; + status: string; + user_created: string; + date_created: string; + user_updated: string | null; + date_updated: string | null; + title: string; + content: OutputData | null; + excerpt: string | null; + featured_image: string | null; + imageUrl?: string | null; +} +export interface HeroSection { + id: string; + hero_title?: string; + hero_subtitle?: string; + hero_cover?: string; + hero_buttons?: HeroButton[]; +} + +interface HeroButton { + label?: string; + link?: string; +} + +interface ItemsQuery { + fields?: string[]; +}