mirror of
https://github.com/OwethuManagedServices/oms-website-nextjs.git
synced 2025-12-17 17:18:09 +00:00
feature: intergrated directus for posts
This commit is contained in:
@ -12,48 +12,37 @@ import WhyChooseUsSection, {
|
|||||||
import FeaturedProductSection, {
|
import FeaturedProductSection, {
|
||||||
defaultObseFeatures,
|
defaultObseFeatures,
|
||||||
} from "./_components/FeaturedProductSection";
|
} from "./_components/FeaturedProductSection";
|
||||||
// import HeroSectionModern from "./_components/HeroSectionModern";
|
import { getHome } from "@/lib/query/home";
|
||||||
// import HeroSectionDynamic from "./_components/HeroSectionDynamic";
|
|
||||||
|
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 (
|
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
|
<HeroSection
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
Where Innovation <br className="hidden md:block" /> Meets
|
{data?.hero_title} <br className="hidden md:block" />
|
||||||
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."
|
subtitle={
|
||||||
buttonText="Learn More"
|
data.hero_subtitle || "Your trusted partner in technology solutions."
|
||||||
buttonHref="/about"
|
} // Use optional chaining and provide a default
|
||||||
imageUrl="/hero-bg.jpg" // Specify your hero image
|
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
|
<CoreServicesSection
|
||||||
title="Core Services"
|
title="Core Services"
|
||||||
|
|||||||
294
app/(website)/tech-talk/[slug]/page.tsx
Normal file
294
app/(website)/tech-talk/[slug]/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,39 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import BlogPostCard from "@/components/BlogPostCard";
|
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";
|
import type { Metadata } from "next";
|
||||||
|
import { getPosts } from "@/lib/query/post";
|
||||||
interface Post {
|
import { Post } from "@/types";
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- SEO Metadata ---
|
// --- SEO Metadata ---
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "OMS TechTalk | Insights & Innovation",
|
title: "OMS TechTalk | Insights & Innovation",
|
||||||
@ -65,8 +34,7 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
// --- Page Component ---
|
// --- Page Component ---
|
||||||
const TechTalkPage = async () => {
|
const TechTalkPage = async () => {
|
||||||
const posts = await getPublishedPosts();
|
const posts: Post[] = await getPosts();
|
||||||
const session = await auth(); // Get session info
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background text-foreground">
|
<div className="bg-background text-foreground">
|
||||||
@ -80,28 +48,21 @@ const TechTalkPage = async () => {
|
|||||||
Insights, trends, and discussions on the latest in technology,
|
Insights, trends, and discussions on the latest in technology,
|
||||||
innovation, and digital transformation from the experts at OMS.
|
innovation, and digital transformation from the experts at OMS.
|
||||||
</p>
|
</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>
|
</div>
|
||||||
{/* Blog Post Grid */}
|
{/* Blog Post Grid */}
|
||||||
{posts.length > 0 ? (
|
{posts.length > 0 ? (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-10">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-10">
|
||||||
{posts.map((post: Post) => (
|
{posts.map((post: Post) => (
|
||||||
<BlogPostCard
|
<BlogPostCard
|
||||||
key={post.id} // Use post ID as key
|
key={post.slug}
|
||||||
slug={post.slug}
|
slug={post.slug}
|
||||||
title={post.title}
|
title={post.title}
|
||||||
excerpt={post.excerpt ?? post.content.substring(0, 150) + "..."}
|
excerpt={post.excerpt ?? "No excerpt available"}
|
||||||
// Use imageUrl from DB or a default placeholder
|
imageUrl={
|
||||||
imageUrl={post.imageUrl ?? "/posts/default-placeholder.jpg"} // Provide a default image
|
post.featured_image ?? "/posts/default-placeholder.jpg"
|
||||||
author={"OMS Team"} // Replace with actual author logic if available (e.g., post.user.name)
|
}
|
||||||
date={new Date(post.createdAt).toLocaleDateString("en-US", {
|
author={"OMS Team"}
|
||||||
|
date={new Date(post.date_created).toLocaleDateString("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
|||||||
@ -20,22 +20,25 @@ const BlogPostCard: React.FC<BlogPostCardProps> = ({
|
|||||||
author,
|
author,
|
||||||
date,
|
date,
|
||||||
}) => {
|
}) => {
|
||||||
console.log("BlogPostCard Props:", { imageUrl });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/tech-talk/${slug}`} passHref>
|
<Link href={`/tech-talk/${slug}`} passHref>
|
||||||
<div className="group bg-card border border-border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-all duration-300 ease-in-out flex flex-col h-full transform hover:-translate-y-1">
|
<div className="group bg-card border border-border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-all duration-300 ease-in-out flex flex-col h-full transform hover:-translate-y-1">
|
||||||
{/* Image Container */}
|
{/* Image Container */}
|
||||||
<div className="relative w-full h-48 overflow-hidden">
|
<div className="relative w-full aspect-video overflow-hidden">
|
||||||
<Image
|
{imageUrl ? (
|
||||||
src={imageUrl}
|
<Image
|
||||||
alt={title}
|
src={imageUrl}
|
||||||
layout="fill"
|
alt={title}
|
||||||
objectFit="cover"
|
fill // Use fill instead of layout
|
||||||
className="transition-transform duration-500 group-hover:scale-105"
|
className="object-cover transition-transform duration-500 group-hover:scale-105" // Use object-cover class
|
||||||
/>
|
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||||
{/* Optional: Subtle overlay on hover */}
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
) : (
|
||||||
|
// Optional: Placeholder if no image
|
||||||
|
<div className="w-full h-full bg-muted flex items-center justify-center">
|
||||||
|
<span className="text-muted-foreground text-sm">No Image</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content Area */}
|
{/* Content Area */}
|
||||||
|
|||||||
5
lib/directus.ts
Normal file
5
lib/directus.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createDirectus, rest } from "@directus/sdk";
|
||||||
|
|
||||||
|
export const directus = createDirectus(
|
||||||
|
String(process.env.DIRECTUS_API_ENDPOINT!)
|
||||||
|
).with(rest());
|
||||||
8
lib/query/home.ts
Normal file
8
lib/query/home.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
66
lib/query/post.ts
Normal file
66
lib/query/post.ts
Normal file
@ -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<Post[]> {
|
||||||
|
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<Post | null> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,10 @@ const nextConfig: NextConfig = {
|
|||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "storage.cvevolve.com",
|
hostname: "storage.cvevolve.com",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: process.env.DIRECTUS_API_ENDPOINT!.replace("https://", ""),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
|
|||||||
24
package-lock.json
generated
24
package-lock.json
generated
@ -9,8 +9,10 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.9.0",
|
"@auth/prisma-adapter": "^2.9.0",
|
||||||
|
"@directus/sdk": "^18.0.3",
|
||||||
"@google/generative-ai": "^0.24.0",
|
"@google/generative-ai": "^0.24.0",
|
||||||
"@prisma/client": "^6.6.0",
|
"@prisma/client": "^6.6.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
"next-auth": "^5.0.0-beta.26",
|
"next-auth": "^5.0.0-beta.26",
|
||||||
@ -646,6 +648,18 @@
|
|||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
|
||||||
@ -4604,6 +4618,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
|
|||||||
@ -10,8 +10,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.9.0",
|
"@auth/prisma-adapter": "^2.9.0",
|
||||||
|
"@directus/sdk": "^18.0.3",
|
||||||
"@google/generative-ai": "^0.24.0",
|
"@google/generative-ai": "^0.24.0",
|
||||||
"@prisma/client": "^6.6.0",
|
"@prisma/client": "^6.6.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
"next-auth": "^5.0.0-beta.26",
|
"next-auth": "^5.0.0-beta.26",
|
||||||
|
|||||||
79
types/index.d.ts
vendored
Normal file
79
types/index.d.ts
vendored
Normal file
@ -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[];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user