From 809f5c6ff7b823cbe4c3414f9b2c17d79a11ca84 Mon Sep 17 00:00:00 2001 From: libertyoms Date: Sat, 26 Apr 2025 19:27:16 +0200 Subject: [PATCH 01/16] feature: intergrated directus for posts --- app/(website)/page.tsx | 57 ++--- app/(website)/tech-talk/[slug]/page.tsx | 294 ++++++++++++++++++++++++ app/(website)/tech-talk/page.tsx | 59 +---- components/BlogPostCard.tsx | 27 ++- lib/directus.ts | 5 + lib/query/home.ts | 8 + lib/query/post.ts | 66 ++++++ next.config.ts | 4 + package-lock.json | 24 ++ package.json | 2 + types/index.d.ts | 79 +++++++ 11 files changed, 530 insertions(+), 95 deletions(-) create mode 100644 app/(website)/tech-talk/[slug]/page.tsx create mode 100644 lib/directus.ts create mode 100644 lib/query/home.ts create mode 100644 lib/query/post.ts create mode 100644 types/index.d.ts 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[]; +} From 1be00d7a4252ca8a9e7fda13f08acd5f6a21b1ba Mon Sep 17 00:00:00 2001 From: libertyoms Date: Sun, 27 Apr 2025 08:51:09 +0200 Subject: [PATCH 02/16] Vacancy added --- actions/apply.ts | 74 ++++ app/(website)/vacancies/[slug]/page.tsx | 56 +++ app/(website)/vacancies/_components/Badge.tsx | 12 + .../vacancies/_components/ListSection.tsx | 31 ++ .../vacancies/_components/MetadataItem.tsx | 22 ++ .../_components/VacancyClientContent.tsx | 341 ++++++++++++++++++ app/api/vacancies/[slug]/route.ts | 19 + app/api/vacancies/route.ts | 9 + components/VacancyApplicationForm.tsx | 302 ++++++++++++++++ components/ui/Button.tsx | 2 +- components/ui/CustomButton.tsx | 19 + components/ui/CustomInput.tsx | 19 + components/ui/CustomLabel.tsx | 18 + components/ui/CustomTextarea.tsx | 19 + constants/index.ts | 9 + lib/demo-data/vacancies.ts | 163 +++++++++ lib/helpers.ts | 13 + package-lock.json | 36 ++ package.json | 2 + types/index.d.ts | 38 ++ 20 files changed, 1203 insertions(+), 1 deletion(-) create mode 100644 actions/apply.ts create mode 100644 app/(website)/vacancies/[slug]/page.tsx create mode 100644 app/(website)/vacancies/_components/Badge.tsx create mode 100644 app/(website)/vacancies/_components/ListSection.tsx create mode 100644 app/(website)/vacancies/_components/MetadataItem.tsx create mode 100644 app/(website)/vacancies/_components/VacancyClientContent.tsx create mode 100644 app/api/vacancies/[slug]/route.ts create mode 100644 app/api/vacancies/route.ts create mode 100644 components/VacancyApplicationForm.tsx create mode 100644 components/ui/CustomButton.tsx create mode 100644 components/ui/CustomInput.tsx create mode 100644 components/ui/CustomLabel.tsx create mode 100644 components/ui/CustomTextarea.tsx create mode 100644 constants/index.ts create mode 100644 lib/demo-data/vacancies.ts create mode 100644 lib/helpers.ts diff --git a/actions/apply.ts b/actions/apply.ts new file mode 100644 index 0000000..e655c50 --- /dev/null +++ b/actions/apply.ts @@ -0,0 +1,74 @@ +"use server"; + +import * as z from "zod"; + +// Re-define or import the schema to validate on the server-side +// Ensure this matches the client-side schema +const applicationSchema = z.object({ + vacancyId: z.string(), + vacancyTitle: z.string(), + firstName: z.string().min(1, "First name is required"), + lastName: z.string().min(1, "Last name is required"), + email: z.string().email("Invalid email address"), + phone: z.string().optional(), + linkedinUrl: z.string().url("Invalid URL").optional().or(z.literal("")), + portfolioUrl: z.string().url("Invalid URL").optional().or(z.literal("")), + coverLetter: z.string().optional(), + // Note: File uploads (resume) are not handled in this basic action. + // Handling files requires FormData and different processing. +}); + +type ApplicationFormData = z.infer; + +interface ActionResult { + success: boolean; + message?: string; +} + +export async function submitApplication( + formData: ApplicationFormData +): Promise { + // 1. Validate data on the server + const validatedFields = applicationSchema.safeParse(formData); + + if (!validatedFields.success) { + console.error( + "Server-side validation failed:", + validatedFields.error.flatten().fieldErrors + ); + return { + success: false, + message: "Invalid data provided. Please check the form.", + // Optionally return specific field errors: errors: validatedFields.error.flatten().fieldErrors + }; + } + + const applicationData = validatedFields.data; + + // 2. Process the application (e.g., save to database, send email) + // For this demo, we'll just log the data. + console.log( + "Received application:", + JSON.stringify(applicationData, null, 2) + ); + + // Simulate processing time + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // In a real application: + // - Save applicationData to your database (e.g., using Prisma or Directus SDK) + // - Handle resume file upload (requires FormData, potentially upload to storage like S3/Minio) + // - Send notification emails (to HR, to the applicant) + + // Example of error handling during processing: + // try { + // await saveApplicationToDatabase(applicationData); + // await sendConfirmationEmail(applicationData.email); + // } catch (error) { + // console.error('Failed to process application:', error); + // return { success: false, message: 'Failed to save application.' }; + // } + + // 3. Return success response + return { success: true, message: "Application submitted successfully!" }; +} diff --git a/app/(website)/vacancies/[slug]/page.tsx b/app/(website)/vacancies/[slug]/page.tsx new file mode 100644 index 0000000..58a3509 --- /dev/null +++ b/app/(website)/vacancies/[slug]/page.tsx @@ -0,0 +1,56 @@ +import { notFound } from "next/navigation"; +import { Vacancy } from "@/types"; +import VacancyClientContent from "../_components/VacancyClientContent"; + +interface ExtendedVacancy extends Vacancy { + company?: { + name: string; + logoUrl?: string; + websiteUrl?: string; + }; + skills?: string[]; +} + +async function getVacancy(slug: string): Promise { + const res = await fetch(`http://localhost:3000/api/vacancies/${slug}`, { + cache: "no-store", + }); + if (!res.ok) { + if (res.status === 404) return null; + console.error(`Failed to fetch vacancy ${slug}: ${res.statusText}`); + throw new Error("Failed to fetch vacancy details"); + } + return res.json(); +} + +interface VacancyDetailsPageProps { + params: { slug: string }; +} + +export default async function VacancyDetailsPage({ + params, +}: VacancyDetailsPageProps) { + const { slug } = await params; + const vacancy = await getVacancy(slug); + + if (!vacancy) { + notFound(); + } + + const shareUrl = `${process.env.WEBSITE_URL}/vacancies/${params.slug}`; + const shareTitle = encodeURIComponent( + `Job Opening: ${vacancy.title} at ${ + vacancy.company?.name || "Owethu Managed Services" + }` + ); + + return ( +
    + +
    + ); +} diff --git a/app/(website)/vacancies/_components/Badge.tsx b/app/(website)/vacancies/_components/Badge.tsx new file mode 100644 index 0000000..64a1393 --- /dev/null +++ b/app/(website)/vacancies/_components/Badge.tsx @@ -0,0 +1,12 @@ +export const Badge = ({ + children, + icon: Icon, +}: { + children: React.ReactNode; + icon?: React.ElementType; +}) => ( + + {Icon && } + {children} + +); diff --git a/app/(website)/vacancies/_components/ListSection.tsx b/app/(website)/vacancies/_components/ListSection.tsx new file mode 100644 index 0000000..26cc056 --- /dev/null +++ b/app/(website)/vacancies/_components/ListSection.tsx @@ -0,0 +1,31 @@ +import { COLORS } from "@/constants"; + +export const ListSection = ({ + title, + items, + icon: Icon, +}: { + title: string; + items?: string[]; + icon?: React.ElementType; +}) => { + if (!items || items.length === 0) return null; + return ( +
    +

    + {Icon && ( + + )} + {title} +

    +
      + {items.map((item, index) => ( +
    • {item}
    • + ))} +
    +
    + ); +}; diff --git a/app/(website)/vacancies/_components/MetadataItem.tsx b/app/(website)/vacancies/_components/MetadataItem.tsx new file mode 100644 index 0000000..f5eeaba --- /dev/null +++ b/app/(website)/vacancies/_components/MetadataItem.tsx @@ -0,0 +1,22 @@ +import { COLORS } from "@/constants"; + +export const MetadataItem = ({ + icon: Icon, + label, + value, +}: { + icon: React.ElementType; + label: string; + value: React.ReactNode; +}) => ( +
    +
    +); diff --git a/app/(website)/vacancies/_components/VacancyClientContent.tsx b/app/(website)/vacancies/_components/VacancyClientContent.tsx new file mode 100644 index 0000000..7d60662 --- /dev/null +++ b/app/(website)/vacancies/_components/VacancyClientContent.tsx @@ -0,0 +1,341 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import Link from "next/link"; +import { Vacancy } from "@/types"; +import VacancyApplicationForm from "@/components/VacancyApplicationForm"; + +import { + FaMapMarkerAlt, + FaBriefcase, + FaClock, + FaCalendarAlt, + FaDollarSign, + FaGraduationCap, + FaShareAlt, + FaCheckCircle, + FaBuilding, + FaLink, + FaListUl, + FaInfoCircle, + FaStar, + FaGift, + FaTools, +} from "react-icons/fa"; +import { formatDate } from "@/lib/helpers"; +import { COLORS } from "@/constants"; +import Image from "next/image"; +import { MetadataItem } from "./MetadataItem"; +import { Badge } from "./Badge"; +import { ListSection } from "./ListSection"; +import Button from "@/components/ui/Button"; + +interface VacancyClientContentProps { + vacancy: Vacancy & { + company?: { name: string; logoUrl?: string; websiteUrl?: string }; + skills?: string[]; + }; + shareUrl: string; + shareTitle: string; +} + +export default function VacancyClientContent({ + vacancy, + shareUrl, + shareTitle, +}: VacancyClientContentProps) { + const [showApplyForm, setShowApplyForm] = useState(false); + const applyFormRef = useRef(null); + + const handleApplyClick = () => { + setShowApplyForm(true); + }; + + useEffect(() => { + if (showApplyForm && applyFormRef.current) { + setTimeout(() => { + applyFormRef.current?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + }, 100); + } + }, [showApplyForm]); + + return ( + // Container is now within the client component +
    + {/* --- Re-include the FULL Company Header --- */} + {vacancy.company && ( +
    +
    + {vacancy.company.logoUrl ? ( + {`${vacancy.company.name} + ) : ( +
    + +
    + )} +
    +

    + {vacancy.title} +

    +

    at {vacancy.company.name}

    + {vacancy.company.websiteUrl && ( + + Visit website{" "} + + + )} +
    +
    + {/* Apply Button in Header (conditional) */} + {!showApplyForm && ( + + )} +
    + )} + {/* --- End Company Header --- */} + + {/* --- Main Grid Layout --- */} +
    + {/* --- Main Content Area (Left Column) --- */} +
    + {/* Title if no company header */} + {!vacancy.company && ( +

    + {vacancy.title} +

    + )} + + {/* Job Description */} +
    +

    + {" "} + Job Description +

    +
    +

    {vacancy.description}

    +
    +
    + + {/* --- All List Sections (Responsibilities, Qualifications, Skills, Benefits) --- */} + + + + + {/* Skills Section - RE-INCLUDED */} + {vacancy.skills && vacancy.skills.length > 0 && ( +
    +

    + {" "} + Skills +

    +
    + {vacancy.skills.map((skill, index) => ( + {skill} + ))} +
    +
    + )} + + {/* Benefits Section - RE-INCLUDED */} + {vacancy.benefits && vacancy.benefits.length > 0 && ( +
    +

    + {" "} + Benefits +

    +
      + {vacancy.benefits.map((item, index) => ( +
    • + + {item} +
    • + ))} +
    +
    + )} + {/* --- End List Sections --- */} + + {/* Apply button below main content (conditional) */} + {!showApplyForm && ( +
    + +
    + )} +
    + {/* --- End Main Content Area --- */} + + {/* --- Sidebar (Right Column) --- */} +
    + {/* Metadata Card - Exactly as before */} +
    +

    + Job Overview +

    +
    + + + {vacancy.location.city}, {vacancy.location.country}{" "} + {vacancy.location.remote && Remote Possible} + + } + /> + {vacancy.employmentType}} + /> + + {vacancy.salary && ( + + {vacancy.salary.min.toLocaleString()} -{" "} + {vacancy.salary.max.toLocaleString()}{" "} + {vacancy.salary.currency} {vacancy.salary.period} + + } + /> + )} + + {vacancy.applicationDeadline && ( + + )} +
    +
    + + {/* Share Card - Exactly as before */} +
    +

    + {" "} + Share this opening +

    + +
    + + {/* --- Conditionally Rendered Application Form Section --- */} + {showApplyForm && ( +
    + {" "} + {/* scroll-mt helps anchor links, lg:mt-0 aligns with other cards */} +

    + Apply for: {vacancy.title} +

    + setShowApplyForm(false)} + /> +
    + )} + {/* --- End Form Section --- */} +
    + {/* --- End Sidebar --- */} +
    + {/* --- End Main Grid --- */} +
    // End Container + ); +} diff --git a/app/api/vacancies/[slug]/route.ts b/app/api/vacancies/[slug]/route.ts new file mode 100644 index 0000000..5cbc86a --- /dev/null +++ b/app/api/vacancies/[slug]/route.ts @@ -0,0 +1,19 @@ +import { NextResponse } from "next/server"; +import { demoVacancies } from "@/lib/demo-data/vacancies"; + +export async function GET( + request: Request, + { params }: { params: { slug: string } } +) { + const slug = params.slug; + // In a real application, you would fetch this data from your CMS (Directus) + const vacancy = demoVacancies.find( + (v) => v.slug === slug && v.status === "Open" + ); + + if (!vacancy) { + return NextResponse.json({ message: "Vacancy not found" }, { status: 404 }); + } + + return NextResponse.json(vacancy); +} diff --git a/app/api/vacancies/route.ts b/app/api/vacancies/route.ts new file mode 100644 index 0000000..bb8e917 --- /dev/null +++ b/app/api/vacancies/route.ts @@ -0,0 +1,9 @@ +import { NextResponse } from "next/server"; +import { demoVacancies } from "@/lib/demo-data/vacancies"; + +export async function GET() { + // In a real application, you would fetch this data from your CMS (Directus) + // For now, we use the demo data + const openVacancies = demoVacancies.filter((v) => v.status === "Open"); + return NextResponse.json(openVacancies); +} diff --git a/components/VacancyApplicationForm.tsx b/components/VacancyApplicationForm.tsx new file mode 100644 index 0000000..40bdc91 --- /dev/null +++ b/components/VacancyApplicationForm.tsx @@ -0,0 +1,302 @@ +// src/components/VacancyApplicationForm.tsx (Example structure and styling) +"use client"; // This likely needs to be a client component for form handling + +import React, { useState, useCallback, ChangeEvent, DragEvent } from "react"; +import { FaUpload, FaFileAlt, FaTimes } from "react-icons/fa"; // Import icons + +interface VacancyApplicationFormProps { + vacancyId: string; + vacancyTitle: string; + // onFormClose?: () => void; // Optional: If you add a close button +} + +export default function VacancyApplicationForm({ + vacancyId, + vacancyTitle, +}: VacancyApplicationFormProps) { + const [formData, setFormData] = useState({ + name: "", + email: "", + phone: "", + linkedin: "", + coverLetter: "", + // Add other fields as needed + }); + const [resumeFile, setResumeFile] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitStatus, setSubmitStatus] = useState< + "idle" | "success" | "error" + >("idle"); + + const handleInputChange = ( + e: ChangeEvent + ) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + // --- File Input Handling --- + const handleFileChange = (e: ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + setResumeFile(e.target.files[0]); + e.target.value = ""; // Reset input value for potential re-upload + } + }; + + const handleDragEnter = useCallback((e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }, []); + + const handleDragLeave = useCallback((e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + }, []); + + const handleDragOver = useCallback( + (e: DragEvent) => { + e.preventDefault(); // Necessary to allow drop + e.stopPropagation(); + if (!isDragging) setIsDragging(true); // Ensure state is true while over + }, + [isDragging] + ); + + const handleDrop = useCallback((e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { + setResumeFile(e.dataTransfer.files[0]); + e.dataTransfer.clearData(); + } + }, []); + + const removeFile = () => { + setResumeFile(null); + }; + // --- End File Input Handling --- + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + setSubmitStatus("idle"); + + const data = new FormData(); + data.append("vacancyId", vacancyId); + data.append("vacancyTitle", vacancyTitle); + data.append("name", formData.name); + data.append("email", formData.email); + data.append("phone", formData.phone); + data.append("linkedin", formData.linkedin); + data.append("coverLetter", formData.coverLetter); + if (resumeFile) { + data.append("resume", resumeFile, resumeFile.name); + } + + try { + // Replace with your actual API endpoint for form submission + const response = await fetch("/api/apply", { + method: "POST", + body: data, // FormData handles multipart/form-data automatically + }); + + if (!response.ok) { + throw new Error("Submission failed"); + } + + setSubmitStatus("success"); + // Optionally reset form: + // setFormData({ name: '', email: '', ... }); + // setResumeFile(null); + // Optionally close form: onFormClose?.(); + } catch (error) { + console.error("Application submission error:", error); + setSubmitStatus("error"); + } finally { + setIsSubmitting(false); + } + }; + + // --- Base Input Styling --- + const inputBaseStyle = + "block w-full px-4 py-2 mt-1 text-gray-700 bg-white border border-gray-300 rounded-md focus:border-gold-500 focus:ring focus:ring-gold-500 focus:ring-opacity-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:focus:border-gold-500 font-poppins"; + + return ( +
    + {/* Example Fields */} +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + + {/* --- Styled File Upload Area --- */} +
    + +
    document.getElementById("resume-upload")?.click()} // Trigger hidden input + > + {/* Hidden Actual Input */} + + + {/* Visual Cue */} + +

    + {isDragging ? "Drop file here" : "Drag & drop your file here"} +

    +

    + or click to browse +

    +

    (PDF, DOC, DOCX, TXT)

    +
    + + {/* Display Selected File */} + {resumeFile && ( +
    +
    + + + {resumeFile.name} + + + ({(resumeFile.size / 1024).toFixed(1)} KB) + +
    + +
    + )} +
    + {/* --- End File Upload Area --- */} + +
    + + +
    + + {/* Submit Button & Status */} +
    + + + {submitStatus === "success" && ( +

    + Application submitted successfully! +

    + )} + {submitStatus === "error" && ( +

    + Submission failed. Please try again. +

    + )} +
    +
    + ); +} diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx index d7ad96c..85b744b 100644 --- a/components/ui/Button.tsx +++ b/components/ui/Button.tsx @@ -20,7 +20,7 @@ const Button: React.FC = ({ }) => { // Base styles including focus ring using semantic vars const baseStyle = - "inline-flex items-center justify-center rounded-lg font-semibold transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"; + "inline-flex items-center justify-center rounded-lg font-semibold transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background cursor-pointer disabled:pointer-events-none disabled:opacity-50 text-sm"; // Variant styles using semantic vars (Tailwind classes generated via @theme) const variantStyles = { diff --git a/components/ui/CustomButton.tsx b/components/ui/CustomButton.tsx new file mode 100644 index 0000000..d65f3ed --- /dev/null +++ b/components/ui/CustomButton.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +const CustomButton = React.forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(({ className, children, ...props }, ref) => { + return ( + + ); +}); +CustomButton.displayName = "CustomButton"; + +export { CustomButton }; diff --git a/components/ui/CustomInput.tsx b/components/ui/CustomInput.tsx new file mode 100644 index 0000000..0f26dc0 --- /dev/null +++ b/components/ui/CustomInput.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +type CustomInputProps = React.InputHTMLAttributes; + +const CustomInput = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +CustomInput.displayName = "CustomInput"; + +export { CustomInput }; diff --git a/components/ui/CustomLabel.tsx b/components/ui/CustomLabel.tsx new file mode 100644 index 0000000..d7984c6 --- /dev/null +++ b/components/ui/CustomLabel.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +type CustomLabelProps = React.LabelHTMLAttributes; + +const CustomLabel = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +
    {/* Submit Button & Status */}
    - + {submitStatus === "success" && (

    - Application submitted successfully! + Application submitted successfully! Closing form...

    )} {submitStatus === "error" && (

    - Submission failed. Please try again. + Submission failed. Please check your details and try again.

    )}
    diff --git a/components/ui/Modal.tsx b/components/ui/Modal.tsx new file mode 100644 index 0000000..5950106 --- /dev/null +++ b/components/ui/Modal.tsx @@ -0,0 +1,126 @@ +// src/components/ui/Modal.tsx +import React, { ReactNode, useEffect } from "react"; +import { FaTimes } from "react-icons/fa"; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title?: string; + children: ReactNode; + size?: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl"; // Optional size control +} + +const Modal: React.FC = ({ + isOpen, + onClose, + title, + children, + size = "2xl", // Default size +}) => { + // Close modal on Escape key press + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + onClose(); + } + }; + if (isOpen) { + document.addEventListener("keydown", handleEscape); + } + // Cleanup listener on component unmount or when modal closes + return () => { + document.removeEventListener("keydown", handleEscape); + }; + }, [isOpen, onClose]); + + // Prevent scrolling on the body when the modal is open + useEffect(() => { + if (isOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "auto"; + } + // Cleanup overflow style on component unmount + return () => { + document.body.style.overflow = "auto"; + }; + }, [isOpen]); + + if (!isOpen) return null; + + // Map size prop to Tailwind max-width classes + const sizeClasses = { + sm: "max-w-sm", + md: "max-w-md", + lg: "max-w-lg", + xl: "max-w-xl", + "2xl": "max-w-2xl", + "3xl": "max-w-3xl", + "4xl": "max-w-4xl", + "5xl": "max-w-5xl", + }; + + return ( + // Backdrop +
    + {/* Modal Panel */} +
    e.stopPropagation()} // Prevent clicks inside the modal from closing it + > + {/* Modal Header (Optional) */} + {title && ( +
    + {title && ( + + )} + +
    + )} + + {/* Modal Body */} +
    + {" "} + {/* Added max-height and scroll */} + {children} +
    +
    + + {/* Add animation keyframes to your global CSS (e.g., src/app/globals.css) */} + +
    + ); +}; + +export default Modal; From b1680d297b5aacaacd70373eeb74496445bbae6d Mon Sep 17 00:00:00 2001 From: libertyoms Date: Sun, 27 Apr 2025 12:47:08 +0200 Subject: [PATCH 04/16] feature: contact us fixed --- app/(website)/contact/page.tsx | 226 +++++++++++++++++++++++----- app/(website)/vacancies/page.tsx | 239 ++++++++++++++++++++++++++++++ components/ContactForm.tsx | 247 +++++++++++++++++++++---------- 3 files changed, 590 insertions(+), 122 deletions(-) create mode 100644 app/(website)/vacancies/page.tsx diff --git a/app/(website)/contact/page.tsx b/app/(website)/contact/page.tsx index 3469adf..cfba5a7 100644 --- a/app/(website)/contact/page.tsx +++ b/app/(website)/contact/page.tsx @@ -1,7 +1,17 @@ -import React from "react"; -import type { Metadata } from "next"; +"use client"; // Needed for FAQ state + +import React, { useState } from "react"; +import { + FaMapMarkerAlt, + FaPhone, + FaEnvelope, + FaClock, + FaChevronDown, + FaChevronUp, +} from "react-icons/fa"; // Using Fa icons for consistency +import { COLORS } from "@/constants"; // Using COLORS constant import ContactForm from "@/components/ContactForm"; -import { FiMapPin, FiPhone, FiMail, FiClock } from "react-icons/fi"; // Import icons +import { Metadata } from "next"; // SEO Metadata for the Contact page export const metadata: Metadata = { @@ -32,17 +42,62 @@ export const metadata: Metadata = { }, }; +// Define the structure for FAQ items +interface FAQItem { + id: number; + question: string; + answer: string; +} + +const faqData: FAQItem[] = [ + { + id: 1, + question: "What services does Owethu Managed Services offer?", + answer: + "We offer a comprehensive range of IT services including custom software development, resource augmentation (IT staffing), project management, cloud solutions, system integration, and IT consulting tailored to various industries.", + }, + { + id: 2, + question: "Which industries do you specialize in?", + answer: + "We have deep expertise in Financial Services & Fintech, Automotive, Technology, and Retail/E-commerce sectors, understanding the unique challenges and opportunities within each.", + }, + { + id: 3, + question: "How can I request a quote for a project?", + answer: + "You can request a quote by filling out the contact form on this page with details about your project requirements, calling us directly, or sending an email to hello@oms.africa. We'll get back to you promptly to discuss your needs.", + }, + { + id: 4, + question: "What are your business hours?", + answer: + "Our standard business hours are Monday to Friday, 8:00 AM to 5:00 PM South African Standard Time (SAST). We are closed on weekends and public holidays.", + }, +]; + // Contact Page Component export default function ContactPage() { + const [openFaqId, setOpenFaqId] = useState(null); + + const toggleFaq = (id: number) => { + setOpenFaqId(openFaqId === id ? null : id); + }; + return ( -
    - {/* Hero Section */} -
    -
    -

    + // Added dark mode base styles +
    + {/* Hero Section - Adjusted padding for mobile */} +
    +
    +
    +

    Get In Touch

    -

    +

    We're here to help! Whether you have a question about our services, need assistance, or want to discuss a project, reach out and let us know. @@ -50,23 +105,31 @@ export default function ContactPage() {

    - {/* Main Content Area (Form + Info) */} -
    -
    -
    - {/* Contact Information Section */} -
    -

    + {/* Main Content Area (Form + Info) - Adjusted padding, added dark mode */} +
    +
    + {/* Grid stacks vertically by default, becomes 2 columns on large screens */} +
    + {/* Contact Information Section - Added dark mode styles */} +
    +

    Contact Information

    - +
    -

    +

    Our Office

    -
    +
    + {/* ... existing address lines ... */} Unit 10 B Centuria Park
    265 Von Willich Avenue @@ -75,12 +138,12 @@ export default function ContactPage() {
    South Africa
    - {/* Optional: Link to Google Maps */} View on Google Maps @@ -88,12 +151,17 @@ export default function ContactPage() {
    - +
    -

    Phone

    +

    + Phone +

    (012) 051 3282 @@ -101,12 +169,17 @@ export default function ContactPage() {
    - +
    -

    Email

    +

    + Email +

    hello@oms.africa @@ -114,38 +187,109 @@ export default function ContactPage() {
    - +
    -

    +

    Business Hours

    -

    +

    Monday - Friday: 8:00 AM - 5:00 PM (SAST)

    -

    +

    Weekends & Public Holidays: Closed

    - {/* Contact Form Section */} -
    -

    + {/* Contact Form Section - Added dark mode styles */} +
    +

    Send Us a Message

    + {/* Assuming ContactForm handles its own dark mode or inherits text colors */}

    - {/* Optional: Map Section Placeholder */} - {/*
    -
    -

    [Embedded Map Placeholder - e.g., Google Maps iframe]

    -
    -
    */} + {/* Map Section - Adjusted padding, added dark mode, responsive height */} +
    +
    +

    + Find Us Here +

    + {/* Adjusted height for different screen sizes */} +
    + +
    +
    +
    + + {/* FAQ Section - Adjusted padding, added dark mode */} +
    +
    +

    + Frequently Asked Questions +

    +
    + {faqData.map((faq) => ( +
    + +
    + {faq.answer} +
    +
    + ))} +
    +
    +

    ); } diff --git a/app/(website)/vacancies/page.tsx b/app/(website)/vacancies/page.tsx new file mode 100644 index 0000000..cc61e4b --- /dev/null +++ b/app/(website)/vacancies/page.tsx @@ -0,0 +1,239 @@ +"use client"; // <-- Add this line to use state + +import Link from "next/link"; +import { useState } from "react"; // <-- Import useState +import { + FaMapMarkerAlt, + FaBriefcase, + FaPaperPlane, + FaArrowRight, + FaRegClock, + FaSearch, +} from "react-icons/fa"; +import { demoVacancies } from "@/lib/demo-data/vacancies"; +import { Vacancy } from "@/types"; +import Button from "@/components/ui/Button"; +import Modal from "@/components/ui/Modal"; // <-- Import your Modal component +import { COLORS } from "@/constants"; +import VacancyApplicationForm from "@/components/VacancyApplicationForm"; + +// Metadata object might need adjustment depending on your setup with client components +// If using App Router, keep it, Next.js handles it. +/* +export const metadata: Metadata = { + title: "Current Vacancies | OMS", + description: + "Explore exciting career opportunities at OMS. Find your perfect role or submit your CV for future consideration.", +}; +*/ + +// Define gold color for consistency (if COLORS.primary is not '#e1c44a', adjust accordingly) +const goldColor = COLORS.primary || "#e1c44a"; // Use COLORS.primary or fallback + +// --- VacancyCard Component (no changes needed here) --- +interface VacancyCardProps { + vacancy: Vacancy; +} + +function VacancyCard({ vacancy }: VacancyCardProps) { + const formatDate = (dateString: string | undefined) => { + if (!dateString) return "Date N/A"; + try { + return new Date(dateString).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + } catch { + return "Invalid Date"; + } + }; + const postedDate = formatDate(vacancy.postedDate); + + return ( + +
    +
    +

    + {vacancy.title} +

    +
    + + + + + {vacancy.postedDate && ( + + + )} +
    +
    +
    + + View Details + +
    +
    + + ); +} +// --- End Vacancy Card Component --- + +// --- Vacancies Page --- +export default function VacanciesPage() { + // TODO: Replace demoVacancies with actual API call if needed client-side, + // or fetch server-side and pass as props if using Pages Router. + // For App Router, `async function` fetches server-side by default. + const vacancies = demoVacancies; + + // --- State for the "Future Positions" Modal --- + const [isFuturePositionModalOpen, setIsFuturePositionModalOpen] = + useState(false); + + const handleOpenFuturePositionModal = () => + setIsFuturePositionModalOpen(true); + const handleCloseFuturePositionModal = () => + setIsFuturePositionModalOpen(false); + // --- End State --- + + return ( +
    + {/* Section 1: Hero / Page Header */} +
    +
    +
    +

    + Career Opportunities +

    +

    + Join our team of innovators and experts. Explore current openings at + OMS or submit your CV for future consideration. +

    +
    +
    + + {/* Section 2: Vacancy List */} +
    +
    +

    + Current Openings +

    + {vacancies.length > 0 ? ( +
    + {vacancies.map((vacancy) => ( + + ))} +
    + ) : ( +
    + +

    + No Open Vacancies Right Now +

    +

    + We're not actively hiring for specific roles at the moment, + but we're always looking for passionate and talented + individuals. Check back soon or submit your CV below! +

    +
    + )} +
    +
    + + {/* Section 3: Future Positions / CV Submission */} +
    +
    + {" "} + {/* Ensure icon contrast */} +

    + {" "} + {/* Ensure heading contrast */} + Don't See the Right Fit? +

    +

    + {" "} + {/* Ensure text contrast */} + We're always looking for talented individuals to join our + journey. Submit your CV, and we'll keep you in mind for future + openings that match your profile. +

    +
    + {/* --- Updated Button --- */} + + {/* --- End Updated Button --- */} +
    +
    +
    + + {/* --- Modal for Future Position Application --- */} + + + +
    + ); +} diff --git a/components/ContactForm.tsx b/components/ContactForm.tsx index 2705599..a9dd6b2 100644 --- a/components/ContactForm.tsx +++ b/components/ContactForm.tsx @@ -1,40 +1,54 @@ "use client"; -import React, { useActionState, useEffect, useRef } from "react"; -import { useFormStatus } from "react-dom"; +import React, { useEffect, useActionState } from "react"; // Removed useRef +import { useForm } from "react-hook-form"; // Removed SubmitHandler +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; import { submitContactForm, ContactFormState } from "@/actions/contact"; -import Button from "@/components/ui/Button"; // Use your existing Button component +import Button from "@/components/ui/Button"; -// Submit button component with pending state -function SubmitButton() { - const { pending } = useFormStatus(); - return ( - - ); -} +// Zod schema for contact form validation +const contactSchema = z.object({ + name: z.string().min(2, "Full name must be at least 2 characters"), + email: z.string().email("Invalid email address"), + subject: z.string().min(3, "Subject must be at least 3 characters"), + message: z.string().min(10, "Message must be at least 10 characters"), +}); + +type ContactFormData = z.infer; -// The main contact form component export default function ContactForm() { + // React Hook Form setup + const { + register, + formState: { errors, isValid }, + reset, + } = useForm({ + resolver: zodResolver(contactSchema), + mode: "onChange", + }); + const initialState: ContactFormState = { message: null, errors: {}, success: false, }; - const [state, dispatch] = useActionState(submitContactForm, initialState); - const formRef = useRef(null); // Ref to reset the form + const [state, formAction] = useActionState(submitContactForm, initialState); // Renamed dispatch to formAction for clarity - // Reset form on successful submission + // Reset form when server action reports success useEffect(() => { if (state.success) { - formRef.current?.reset(); + reset(); } - }, [state.success]); + }, [state.success, reset]); + // Removed onSubmit handler + + // Pass formAction directly to the form's action prop + // Remove onSubmit={handleSubmit(onSubmit)} return ( -
    - {/* Name Input */} + + {/* Name Field */}
    - {/* Email Input */} + {/* Email Field */}
    - {/* Subject Input */} + {/* Subject Field */}
    - {/* Message Textarea */} + {/* Message Field */}
    - - {/* General Form Message (Success or Error) */} -
    - {state.message && ( + className={`block w-full px-4 py-2 border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground resize-vertical dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:placeholder-gray-400 ${ + errors.message + ? "border-destructive" + : "border-border dark:border-gray-600" + }`} + /> + {errors.message && ( )} + {state.errors?.message && + state.errors.message.map((err: string) => ( +

    + {err} +

    + ))}
    + {/* General Form Response */} + {state.message && ( +

    + {state.message} +

    + )} + {/* Submit Button */} + {/* isSubmitting from useFormState is now implicitly handled by the form action */}
    - +
    ); From 9b0cf2a5f6174f8563d287eae7eb08fe1cac57f4 Mon Sep 17 00:00:00 2001 From: libertyoms Date: Sun, 27 Apr 2025 13:34:34 +0200 Subject: [PATCH 05/16] Darkmode omplemented --- app/(website)/about/page.tsx | 116 ++++++++++++++++----------------- app/(website)/contact/page.tsx | 30 --------- components/HeaderClient.tsx | 4 +- components/HeaderSingle.tsx | 2 +- 4 files changed, 61 insertions(+), 91 deletions(-) diff --git a/app/(website)/about/page.tsx b/app/(website)/about/page.tsx index 917dd0c..a067af8 100644 --- a/app/(website)/about/page.tsx +++ b/app/(website)/about/page.tsx @@ -131,13 +131,13 @@ const industryExpertise = [ export default function AboutUsPage() { return ( -
    +
    {" "} - {/* Prevent horizontal scroll */} + {/* Prevent horizontal scroll & Add dark mode base */} {/* Section 1: Hero / Company Overview */}
    {/* Optional decorative background elements */} -
    +
    About Owethu Managed Services

    -

    +

    Your strategic partner in navigating the digital frontier. We fuse deep technical expertise with a passion for innovation to craft bespoke IT solutions that drive tangible business results and unlock @@ -159,15 +159,15 @@ export default function AboutUsPage() {

    {/* Section 2: Our Genesis & Purpose */} -
    +
    -

    +

    Forged in Innovation,
    Driven by Purpose

    -

    +

    Expand on the founding story/motivation. E.g., Owethu Managed Services was born from a clear vision: to harness the transformative power of technology not just to solve problems, @@ -176,7 +176,7 @@ export default function AboutUsPage() { technology's potential and the unique challenges modern organizations face.

    -

    +

    Elaborate on the journey. E.g., Our journey has been defined by a relentless pursuit of knowledge, adaptation to the ever-evolving tech landscape, and an unwavering commitment to @@ -198,12 +198,12 @@ export default function AboutUsPage() {

    {/* Section 3: Vision & Mission (Similar layout, refined look) */} -
    +
    -

    +

    Our North Star

    -

    +

    Guiding our strategy, actions, and partnerships.

    @@ -211,17 +211,17 @@ export default function AboutUsPage() {
    {/* Vision Card */}
    -

    +

    Our Vision

    -

    +

    "To be global leaders in delivering cutting-edge IT solutions, pushing the boundaries of what's possible, and transforming industries for the better. We aim to empower @@ -231,17 +231,17 @@ export default function AboutUsPage() {

    {/* Mission Card */}
    -

    +

    Our Mission

    -

    +

    "We are dedicated to creating tailored, innovative solutions that drive business success. By combining expertise with cutting-edge technology, we solve complex problems and @@ -254,13 +254,13 @@ export default function AboutUsPage() {

    {/* Section 4: Our Approach & Methodology */} -
    +
    -

    +

    How We Deliver Excellence

    -

    +

    Our methodology is built on collaboration, precision, and a relentless focus on delivering impactful results. We tailor our process to fit your specific project needs. @@ -270,16 +270,16 @@ export default function AboutUsPage() { {ourApproach.map((item) => (

    -

    +

    {item.title}

    -

    +

    {item.description}

    @@ -288,7 +288,7 @@ export default function AboutUsPage() {
    {/* Section 5: Industry Expertise */} -
    +
    @@ -296,10 +296,10 @@ export default function AboutUsPage() { className="text-5xl text-gold-500 mb-4" style={{ color: "#e1c44a" }} /> -

    +

    Deep Domain Knowledge

    -

    +

    We combine broad technological capabilities with specialized expertise across key industries, understanding the unique challenges and opportunities within each sector. @@ -309,13 +309,13 @@ export default function AboutUsPage() { {industryExpertise.map((exp) => (

    -

    +

    {exp.industry}

    -

    +

    {exp.details}

    @@ -331,7 +331,7 @@ export default function AboutUsPage() { className="text-5xl text-gold-500 mx-auto mb-5" style={{ color: "#e1c44a" }} /> -

    +

    Smart Technology, Applied Wisely

    @@ -339,38 +339,38 @@ export default function AboutUsPage() { strategically. Our focus is on building solutions that are:

    -
    +

    Scalable & Future-Proof

    -

    +

    Designed to grow with your business and adapt to future technological advancements.

    -
    +

    Robust & Reliable

    -

    +

    Built with quality and security at the core, ensuring dependable performance.

    -
    +

    Best-Fit Solutions

    -

    +

    Chosen based on your specific needs, budget, and existing infrastructure, not just the latest hype.

    @@ -379,25 +379,25 @@ export default function AboutUsPage() {
    {/* Section 7: Core Values (Reusing previous structure, adjusting background) */} -
    +
    -

    +

    Our Foundational Values

    {coreValues.map((value) => (
    -

    +

    {value.title}

    -

    +

    {value.description}

    @@ -406,7 +406,7 @@ export default function AboutUsPage() {
    {/* Section 8: The OMS Partnership */} -
    +
    @@ -424,17 +424,17 @@ export default function AboutUsPage() { className="text-5xl text-gold-500 mb-4" style={{ color: "#e1c44a" }} /> -

    +

    More Than Vendors,
    {" "} We're Partners

    -

    +

    We believe the best results come from true collaboration. We invest time in understanding your culture, goals, and challenges, working alongside your team as an extension of your own capabilities.

    -

    +

    This means open communication, shared goals, proactive problem-solving, and a long-term commitment to your success, extending far beyond project completion. @@ -444,13 +444,13 @@ export default function AboutUsPage() {

    {/* Section 9: Our Leadership Team */} -
    +
    -

    +

    Meet Our Leadership

    -

    +

    Slightly enhance the intro. E.g., Guided by experience and a passion for innovation, our leadership team fosters a culture of excellence and empowers our experts to deliver outstanding results @@ -463,7 +463,7 @@ export default function AboutUsPage() { {leadershipTeam.map((member) => (

    {/* Added max-width and mx-auto for centering if fewer than 3 items */}
    @@ -473,11 +473,11 @@ export default function AboutUsPage() { alt={`Photo of ${member.name}`} layout="fill" objectFit="cover" - className="bg-gray-200" + className="bg-gray-200 dark:bg-gray-600" />
    -

    +

    {member.name}

    {member.title}

    -

    +

    {member.bio}

    {member.linkedinUrl && member.linkedinUrl !== "#" && ( @@ -494,7 +494,7 @@ export default function AboutUsPage() { href={member.linkedinUrl} target="_blank" rel="noopener noreferrer" - className="text-sm font-medium text-blue-600 hover:text-blue-800 transition-colors duration-200 font-poppins" + className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 transition-colors duration-200 font-poppins" aria-label={`LinkedIn profile of ${member.name}`} > {/* Using a simple text link for cleanliness */} @@ -510,15 +510,15 @@ export default function AboutUsPage() {
    {/* Section 10: Commitment to Impact (Optional, but adds value) */}
    - -

    + +

    Driving Measurable Impact

    -

    +

    Ultimately, our success is measured by yours. We are committed to delivering solutions that not only meet technical requirements but also drive efficiency, foster growth, enhance user experiences, and @@ -529,7 +529,7 @@ export default function AboutUsPage() {

    Start the Conversation diff --git a/app/(website)/contact/page.tsx b/app/(website)/contact/page.tsx index cfba5a7..67c5b9a 100644 --- a/app/(website)/contact/page.tsx +++ b/app/(website)/contact/page.tsx @@ -11,36 +11,6 @@ import { } from "react-icons/fa"; // Using Fa icons for consistency import { COLORS } from "@/constants"; // Using COLORS constant import ContactForm from "@/components/ContactForm"; -import { Metadata } from "next"; - -// SEO Metadata for the Contact page -export const metadata: Metadata = { - title: "Contact Us | Owethu Managed Services (OMS)", - description: - "Get in touch with Owethu Managed Services. Contact us for IT solutions, resource augmentation, project management, and custom software development inquiries.", - alternates: { - canonical: "/contact", - }, - openGraph: { - title: "Contact OMS", - description: "Reach out to OMS for expert IT services and solutions.", - url: "https://oms.africa/contact", // Replace with your actual domain - images: [ - { - url: "/og-image-contact.jpg", // Create a specific OG image for contact - width: 1200, - height: 630, - alt: "Contact Owethu Managed Services", - }, - ], - }, - twitter: { - card: "summary_large_image", - title: "Contact OMS", - description: "Get in touch with Owethu Managed Services.", - images: ["/og-image-contact.jpg"], - }, -}; // Define the structure for FAQ items interface FAQItem { diff --git a/components/HeaderClient.tsx b/components/HeaderClient.tsx index a65efbb..fa537d5 100644 --- a/components/HeaderClient.tsx +++ b/components/HeaderClient.tsx @@ -153,7 +153,7 @@ const HeaderClient = ({ title="Request a Demo" > - Request Demo + Request OBSE Demo {/* --- Auth Section --- */} @@ -333,7 +333,7 @@ const HeaderClient = ({ title="Request a Demo" > - Request Demo + Request OBSE Demo
    {/* Auth Buttons in Mobile Menu */} diff --git a/components/HeaderSingle.tsx b/components/HeaderSingle.tsx index cea8e29..50ebc0c 100644 --- a/components/HeaderSingle.tsx +++ b/components/HeaderSingle.tsx @@ -164,7 +164,7 @@ const Header = () => { title="Request a Demo" > - Request Demo + Request OBSE Demo

    From 77260b1f7ac18d1038a706f1c54e2ad1a25463d6 Mon Sep 17 00:00:00 2001 From: libertyoms Date: Sun, 27 Apr 2025 16:57:53 +0200 Subject: [PATCH 06/16] OBSE page completed --- app/(website)/obse/page-backup.tsx | 959 +++++++++++++++++++++++++++++ app/(website)/obse/page.tsx | 798 ++++++++++++++++++++++++ app/(website)/vacancies/page.tsx | 19 +- components/HeaderClient.tsx | 2 +- 4 files changed, 1766 insertions(+), 12 deletions(-) create mode 100644 app/(website)/obse/page-backup.tsx create mode 100644 app/(website)/obse/page.tsx diff --git a/app/(website)/obse/page-backup.tsx b/app/(website)/obse/page-backup.tsx new file mode 100644 index 0000000..6899c52 --- /dev/null +++ b/app/(website)/obse/page-backup.tsx @@ -0,0 +1,959 @@ +// app/obse/page.tsx + +import Image from "next/image"; +import Link from "next/link"; // Import Link for internal navigation +import { + FaBolt, // Represents speed, automation + FaExclamationTriangle, // Represents challenges, risks + FaLightbulb, // Represents solution, intelligence + FaCogs, // Represents how it works, engine + FaThumbsUp, // Represents benefits, advantages + FaChartPie, // Represents data outputs, insights + FaMicrochip, // Represents technology + FaBriefcase, // Represents use cases + FaUsersCog, // Represents customization, partnership + FaInfoCircle, // Represents about section (alternative) + FaPhoneAlt, // Represents contact + FaPlayCircle, // Optional for mini-CTA + FaArrowRight, // For CTAs or steps + FaFilePdf, // For PDF/Statement icon + FaSearchDollar, // For income detection + FaShieldAlt, // For fraud detection + FaNetworkWired, // For API integration + FaTable, // For data structuring + FaBrain, // For Machine Learning + FaBusinessTime, // For turnaround time + FaHourglassHalf, // For reducing time + FaHandHoldingUsd, // For operational costs + FaUserCheck, // For accuracy + FaChartLine, // For efficiency/profitability + FaClipboardList, // For transaction summary + FaCalendarAlt, // For debit orders + FaMoneyBillWave, // For disposable income + FaBalanceScale, // For affordability + FaExchangeAlt, // For Income/Expenses + FaEye, // For review/interact step + FaUpload, // For upload step + FaPaperPlane, + FaCheckCircle, // For access/integrate step +} from "react-icons/fa"; + +const keyFeatures = [ + { + icon: FaBolt, + title: "Automated Data Extraction", + description: + "Processes statements in seconds (avg. 9-12s), drastically reducing manual effort.", + }, + { + icon: FaUserCheck, // Using accuracy icon here + title: "Advanced OCR Technology", + description: + "High accuracy, reads varied fonts, handles stamps, and distinguishes similar characters ('7' vs 'Z').", + }, + { + icon: FaFilePdf, + title: "Comprehensive SA Bank Coverage", + description: + "Handles statements from all major SA banks and various formats (PDF, scanned, stamped, multi/single page).", + }, + { + icon: FaSearchDollar, + title: "Intelligent Income Detection", + description: + "Auto-identifies salaried/non-salaried income with suggestive logic and exclusionary rules.", + }, + { + icon: FaShieldAlt, + title: "Enhanced Fraud Detection", + description: + "Detects document tampering, fraudulent transaction insertion, and developing ML-based behavioral profiling.", + }, + { + icon: FaCogs, // Using cogs for interaction/processing + title: "Dynamic Data Interaction", + description: + "User-friendly interface for real-time transaction recategorization and immediate recalculations.", + }, + { + icon: FaNetworkWired, + title: "Seamless API Integration", + description: + "Push structured, validated data directly into your credit scoring, LOS, or other internal systems.", + }, + { + icon: FaChartPie, + title: "Visual Analytics & Reporting", + description: + "Intuitive dashboards showing cash flow, income sources, and spending patterns.", + }, + { + icon: FaTable, // Using table for dashboard data + title: "MI Dashboard & Performance Tracking", + description: + "Monitor processing volumes, success rates, processing times, and user performance.", + }, + { + icon: FaTable, + title: "Multiple Export Formats", + description: + "Download results as structured Excel, formatted PDFs, or copy data for easy transfer.", + }, + { + icon: FaCheckCircle, // Reusing from About Us for validation + title: "Built-in Accuracy Checks", + description: + "Includes automated validations and data reviews for uncertain extractions to ensure reliability.", + }, +]; + +const coreBenefits = [ + { + icon: FaHourglassHalf, + title: "Dramatically Reduce Processing Time", + description: + "Cut down statement analysis from hours to mere minutes/seconds.", + }, + { + icon: FaUserCheck, + title: "Ensure High Accuracy & Reliability", + description: + "Minimize human error with advanced OCR and built-in validation rules.", + }, + { + icon: FaHandHoldingUsd, + title: "Lower Operational Costs", + description: + "Reduce reliance on manual data entry staff and associated overheads.", + }, + { + icon: FaShieldAlt, + title: "Strengthen Fraud Prevention", + description: + "Proactively identify tampered documents and suspicious financial activity.", + }, + { + icon: FaBusinessTime, + title: "Accelerate Turnaround Times", + description: + "Speed up loan applications, credit assessments, and customer onboarding.", + }, + { + icon: FaBrain, // Using brain for better decisions + title: "Improve Decision-Making", + description: + "Base assessments on accurate, comprehensive, and rapidly available data.", + }, + { + icon: FaChartLine, + title: "Enhance Operational Efficiency", + description: "Streamline workflows, free up staff for higher-value tasks.", + }, + { + icon: FaThumbsUp, // Using thumbs up for compliance + title: "Improve Compliance", + description: + "Ensure consistent and accurate data handling for regulatory requirements.", + }, +]; + +const dataOutputs = [ + { + icon: FaClipboardList, + title: "Basic Transaction Summary", + description: + "Concise overview of transactions for spending pattern analysis.", + }, + { + icon: FaCalendarAlt, + title: "Debit Order Analysis", + description: "Clear tracking of recurring payments and regular expenses.", + }, + { + icon: FaMoneyBillWave, + title: "Disposable Income Calculation", + description: "Assessment of available funds after essential expenses.", + }, + { + icon: FaBalanceScale, + title: "Affordability Assessment Data", + description: "Key data points to support financial capability evaluation.", + }, + { + icon: FaExchangeAlt, + title: "Income & Expenses Breakdown", + description: "Detailed insights into financial inflows and outflows.", + }, + { + icon: FaTable, + title: "Detailed Transaction List", + description: + "Full, structured transaction data exportable for deep analysis.", + }, + { + icon: FaExclamationTriangle, + title: "Fraud Indicators", + description: + "Flags for potentially suspicious documents or transaction patterns.", + }, +]; + +const useCases = [ + { title: "Lending & Credit Origination (Personal, VAF, SME)" }, + { title: "Credit Risk Assessment & Scoring Input" }, + { title: "Automated Income Verification" }, + { title: "Affordability Calculations & Compliance Checks" }, + { title: "Faster Customer Onboarding (KYC/Financial)" }, + { title: "Portfolio Financial Health Monitoring" }, + { title: "Internal Audit & Reconciliation Support" }, + { title: "Fraud Investigation Data Preparation" }, +]; + +export default function ObsePage() { + return ( + <> +
    +
    +
    +
    +
    + OMS Logo +
    +

    + Revolutionize Lending with OBSE +

    +

    + Automate data extraction, enhance accuracy, detect fraud, and + accelerate decision-making with South Africa's intelligent bank + statement solution: Owethu Bank Statement Extraction. +

    +
    + + Request a Demo + + + See How It Works + +
    +

    + Keywords: Bank Statement Extraction, OCR, Automation, Financial Data + Analysis, South Africa +

    +
    +
    + +
    +
    +
    +
    + +

    + The High Cost of Manual Bank Statement Processing +

    +

    + In the rapidly evolving landscape of financial services, where + precision and speed are paramount, extracting data from bank + statements manually creates significant roadblocks. Efficiently + parsing financial information is critical for accurate risk + assessment, yet reliance on manual methods impacts efficiency + and decision-making. +

    +
      +
    • + + + + Slow & Labor-Intensive: + {" "} + Countless hours spent on manual data entry lead to + operational bottlenecks. + +
    • +
    • + {" "} + {/* Using red for error */} + + + Prone to Human Error: + {" "} + Mistakes risk inaccurate assessments, compliance breaches, + and poor decisions. + +
    • +
    • + + + + High Operational Costs: + {" "} + Significant resources consumed managing large volumes + manually. + +
    • +
    • + {" "} + {/* Using red for ineffective fraud detection */} + + + Limited Fraud Detection: + {" "} + Manually identifying sophisticated fraud is difficult and + time-consuming. + +
    • +
    • + + + + Long Turnaround Times: + {" "} + Slow processing leads to customer dissatisfaction and lost + opportunities. + +
    • +
    +
    +
    + Manual data entry challenges vs automated efficiency +
    +
    +
    +
    +
    + + {/* --- 3. Introducing OBSE: The Intelligent Solution --- */} +
    +
    +
    +
    + {/* Use the UI screenshot showing upload */} + OBSE Bank Statement Upload Interface +
    +
    + +

    + OBSE: Automated Accuracy, Speed, and Insight +

    +

    + Owethu Bank Statement Extraction (OBSE) is an advanced OCR and + data aggregation platform designed specifically for the South + African financial landscape. It seamlessly extracts + comprehensive, accurate data from{" "} + + any SA bank statement + {" "} + (individual or juristic), regardless of format – including PDF + templates, scanned images, stamped documents, and even + disorganized statements. +

    +

    + OBSE transforms raw bank statement data into actionable + financial intelligence in seconds, eliminating manual drudgery + and empowering your teams to make faster, smarter decisions. +

    +
    +
    +
    +
    + + {/* --- 4. How OBSE Works: Simple Steps, Powerful Results --- */} +
    +
    +
    + +

    + From PDF to Insights in Seconds +

    +

    + OBSE simplifies complex data extraction into a streamlined + workflow: +

    +
    + {/* Process Steps - Can use a grid or timeline structure */} +
    + {/* Step 1: Upload */} +
    +
    + +
    +

    + 1. Upload +

    +

    + Easily upload single or multiple bank statements (PDFs). +

    +
    + {/* Step 2: Extract */} +
    +
    + +
    +

    + 2. Automated Extraction +

    +

    + Powerful engine reads, structures, and validates data in seconds +

    +
    + {/* Step 3: Analyze & Check */} +
    +
    + +
    +

    + 3. Analysis & Fraud Check +

    +

    + Analyzes transactions, identifies income, flags potential fraud. +

    +
    + {/* Step 4: Review */} +
    +
    + +
    +

    + 4. Review & Interact (Optional) +

    +

    + User-friendly interface to review, recategorize (real-time), and + view insights. +

    +
    + {/* Step 5: Access/Integrate */} +
    +
    + +
    +

    + 5. Access & Integrate +

    +

    + Export (Excel, PDF), use visual analytics, or integrate via API. +

    +
    +
    + {/* Visual for the process flow */} +
    + OBSE Process Flow Visualization +

    + Process Flow Visual (Replace with Image) +

    +
    +
    +
    + + {/* --- 5. Key Features: Powering Your Processes --- */} +
    +
    +
    + {/* */} +

    + Unlock Deeper Financial Understanding +

    +

    + Explore the core capabilities that make OBSE an indispensable tool + for modern financial analysis. +

    +
    +
    + {keyFeatures.map((feature) => ( +
    + +

    + {feature.title} +

    +

    + {feature.description} +

    +
    + ))} +
    +
    +
    + + {/* --- 6. Core Benefits: The OBSE Advantage (The Gain) --- */} +
    +
    +
    + +

    + Transform Operations & Decision-Making +

    +

    + Discover the tangible benefits OBSE brings to your financial + workflows and bottom line. +

    +
    +
    + {coreBenefits.map((benefit) => ( +
    + +

    + {benefit.title} +

    +

    + {benefit.description} +

    +
    + ))} +
    +
    +
    + + {/* --- 7. Data Outputs & Insights --- */} +
    +
    +
    +
    + +

    + Actionable Intelligence at Your Fingertips +

    +

    + OBSE delivers a suite of standard financial summaries and + detailed data points essential for informed decisions. Gain + clarity on: +

    +
    + {dataOutputs.map((output) => ( +
    + +
    +

    + {output.title} +

    +

    + {output.description} +

    +
    +
    + ))} +
    +
    +
    + {/* Visual: Use dashboard examples from Page 6 */} + OBSE Dashboard Examples showing financial insights +

    + Dashboard Visual (Replace with Image) +

    +
    +
    +
    +
    + + {/* --- 8. Technology Deep Dive --- */} +
    +
    +
    + +

    + Leveraging Advanced Technology +

    +

    + Built on a foundation of robust and intelligent technologies for + reliable, accurate results. +

    +
    +
    + {/* OCR Engine */} +
    + +

    + Advanced OCR Engine +

    +

    + Fine-tuned for SA bank statements, handles diverse formats, + scans, and obscured text. +

    +
    + {/* Machine Learning */} +
    + +

    + Machine Learning +

    +

    + Continuously improves interpretation, reconciles discrepancies, + and powers fraud detection models. +

    +
    + {/* Data Structuring */} +
    + +

    + Data Structuring & Validation +

    +

    + Intelligently structures extracted data (e.g., JSON), validates + entries, applies rules. +

    +
    + {/* Secure API */} +
    + +

    + Secure API +

    +

    + Robust and secure API for seamless, safe integration with your + existing systems. +

    +
    +
    + {/* Visual: JSON code snippet visual from Page 5 */} +
    + Example JSON output from OBSE data extraction +

    + JSON Output Example (Replace with Image) +

    +
    +
    +
    + + {/* --- 9. Use Cases & Applications --- */} +
    +
    +
    + +

    + Empowering Various Financial Processes +

    +

    + OBSE delivers value across a wide range of applications within the + financial services sector. +

    +
    +
    + {useCases.map((useCase) => ( +
    + + + {useCase.title} + +
    + ))} +
    +
    +
    + + {/* --- 10. Customization & Partnership --- */} +
    +
    +
    +
    + OMS and Client Partnership +
    +
    + +

    + Flexible Solutions, Tailored To You +

    +

    + While OBSE offers a comprehensive standard package, we + understand that each organization has unique needs. If you + require specific information or analysis not included,{" "} + + we are committed to collaborating with you to develop + tailor-made outputs + {" "} + that align perfectly with your requirements. +

    +

    + Our goal is to ensure OBSE meets and exceeds your expectations, + providing the exact insights needed for your operational + processes and decision-making. +

    + {/* POC Callout */} +
    +

    + Proof of Concept (POC) Available +

    +

    + We offer Proof of Concept engagements to demonstrate OBSE's + capabilities with your specific data and workflows before full + commitment. Let's discuss your needs. +

    +
    +
    +
    +
    +
    + + {/* --- 11. About OMS: Your Trusted Technology Partner --- */} +
    +
    + {/* Optional: Simple OMS logo here */} + Owethu Management Solutions Logo +

    + Your Experienced Technology Partner +

    +

    + Owethu Management Solutions (OMS) provides professional technology + services with a proven track record, including participation in + ABSA's supplier development program since 2020. OBSE is backed by + our dedicated team of IT Managers, Senior Developers, and UX + Designers committed to robust, user-friendly solutions. +

    +

    + We focus on leveraging technology to solve real-world business + challenges in the financial sector, emphasizing efficiency, + accuracy, and strong partnerships. +

    +
    + + Learn More About OMS + +
    +
    +
    + + {/* --- 12. Call to Action (CTA) --- */} +
    +
    + +

    + Ready to Transform Your Bank Statement Processing? +

    +

    + See OBSE in action! Schedule a live demo tailored to your specific + use case and discover how OBSE can streamline your operations, + reduce costs, and mitigate risk. +

    +
    + + Request a Personalized Demo + + + Contact Sales Team + +
    + {/* Optional Tertiary Link */} + {/* */} +
    +
    + + {/* --- 13. Contact Information --- */} +
    +
    +
    + +

    + Get in Touch +

    +

    + For personalized support or to schedule your demo, reach out to + us. Let's harness the power of automated data extraction for your + business. +

    +
    +
    +
    +

    + Contact Details: +

    +

    + Phone:{" "} + + (012) 051 3281 + +

    +

    + Email:{" "} + + Zanelem@oms.africa + +

    +
    + + {/* Optional: Placeholder for Contact Form */} + {/*
    +

    Or Send Us a Message

    +

    Contact form component would go here.

    + Go to Contact Form +
    */} +
    +
    +
    + + ); +} diff --git a/app/(website)/obse/page.tsx b/app/(website)/obse/page.tsx new file mode 100644 index 0000000..b0a4479 --- /dev/null +++ b/app/(website)/obse/page.tsx @@ -0,0 +1,798 @@ +import Image from "next/image"; +import Link from "next/link"; +import { + FaArrowRight, + FaCheckCircle, + FaCogs, + FaDatabase, + FaEnvelope, + FaExclamationTriangle, + FaFileAlt, + FaFileInvoiceDollar, + FaFingerprint, + FaHandshake, + FaHistory, + FaLaptopCode, + FaLayerGroup, + FaLightbulb, + FaPhone, + FaPlayCircle, + FaPuzzlePiece, + FaSearchDollar, + FaShieldAlt, + FaShippingFast, + FaSignInAlt, + FaSpinner, + FaSyncAlt, + FaTachometerAlt, + FaUserCheck, + FaUsersCog, + FaWrench, +} from "react-icons/fa"; +import { COLORS } from "@/constants"; // Assuming COLORS constant is available +import ContactForm from "@/components/ContactForm"; // Assuming ContactForm is available + +// Define structure for features, benefits, etc. if needed for mapping +interface FeatureItem { + icon: React.ElementType; + title: string; + description: string; + isComingSoon?: boolean; +} + +const keyFeatures: FeatureItem[] = [ + { + icon: FaShippingFast, + title: "Automated Data Extraction", + description: + "Processes statements in seconds (avg. 9-12s), drastically reducing manual effort.", + }, + { + icon: FaFileAlt, + title: "Advanced OCR Technology", + description: + "High accuracy, handles various fonts, obscured text (stamps), and similar characters.", + }, + { + icon: FaDatabase, + title: "Comprehensive SA Bank Coverage", + description: + "Handles statements from all major SA banks and various formats (PDF, scanned, stamped).", + }, + { + icon: FaSearchDollar, + title: "Intelligent Income Detection", + description: + "Identifies salaried/non-salaried income, provides explanations, and filters out transfers.", + }, + { + icon: FaFingerprint, + title: "Enhanced Fraud Detection", + description: + "Detects document tampering, fraudulent insertions, and developing behavioral profiling.", + isComingSoon: true, + }, + { + icon: FaSyncAlt, + title: "Dynamic Data Interaction", + description: + "User-friendly interface for real-time transaction recategorization with immediate recalculations.", + }, + { + icon: FaLaptopCode, + title: "Seamless API Integration", + description: + "Push structured, validated data directly into your credit scoring or loan origination systems.", + }, + { + icon: FaTachometerAlt, + title: "Visual Analytics & Reporting", + description: + "Intuitive dashboards for insights into cash flow, income sources, and spending patterns.", + }, + { + icon: FaUsersCog, + title: "MI Dashboard & Performance Tracking", + description: + "Monitor processing volumes, success rates, average times, and user performance.", + }, + { + icon: FaSignInAlt, + title: "Multiple Export Formats", + description: + "Download results as structured Excel, formatted PDFs, or copy data easily.", + }, + { + icon: FaCheckCircle, + title: "Built-in Accuracy Checks", + description: + "Includes automated validations and data reviews for uncertain extractions.", + }, + { + icon: FaHistory, + title: "Comprehensive Audit Trail", + description: + "Maintains a detailed log of processing steps and user interactions for compliance and traceability.", + }, +]; + +const coreBenefits: FeatureItem[] = [ + { + icon: FaShippingFast, + title: "Dramatically Reduce Processing Time", + description: "Cut statement analysis from hours to minutes/seconds.", + }, + { + icon: FaCheckCircle, + title: "Ensure High Accuracy & Reliability", + description: "Minimize human error with advanced OCR and validation.", + }, + { + icon: FaFileInvoiceDollar, + title: "Lower Operational Costs", + description: "Reduce reliance on manual data entry staff and overheads.", + }, + { + icon: FaShieldAlt, + title: "Strengthen Fraud Prevention", + description: + "Proactively identify tampered documents and suspicious activity.", + }, + { + icon: FaTachometerAlt, + title: "Accelerate Turnaround Times", + description: + "Speed up loan applications, credit assessments, and onboarding.", + }, + { + icon: FaLightbulb, + title: "Improve Decision-Making", + description: "Base assessments on accurate, comprehensive, fast data.", + }, + { + icon: FaCogs, + title: "Enhance Operational Efficiency", + description: + "Streamline workflows and free up staff for higher-value tasks.", + }, + { + icon: FaUserCheck, + title: "Improve Compliance", + description: "Ensure consistent and accurate data handling.", + }, +]; + +const dataOutputs = [ + "Basic Transaction Summary", + "Debit Order Analysis", + "Disposable Income Calculation", + "Affordability Assessment Data", + "Income & Expenses Breakdown", + "Detailed Transaction List (Exportable)", + "Fraud Indicators & Flags", +]; + +const useCases = [ + "Lending & Credit Origination (Personal, Vehicle, SME)", + "Credit Risk Assessment", + "Income Verification Automation", + "Affordability Calculations", + "Customer Onboarding (KYC)", + "Financial Health Monitoring", + "Internal Audit & Reconciliation", + "Fraud Investigation Support", +]; + +export default function ObsePage() { + return ( +
    + {/* 1. Hero Section */} +
    +
    + {/* Optional: Add a subtle background pattern or image */} + {/*
    */} +
    + {/* Consider adding OMS Logo here */} + {/* OMS Logo */} +

    + Revolutionize Your Lending and Credit Processes with OBSE +

    +

    + Automate data extraction, enhance accuracy, detect fraud, and + accelerate decision-making with South Africa's intelligent bank + statement solution. +

    +
    + + See How It + Works + + {/* Optional Mini-CTA */} + {/* + Watch a Quick Overview + */} +
    +
    +
    + + {/* 2. The Challenge Section */} +
    +
    +
    +

    + The High Cost of Manual Bank Statement Processing +

    +

    + In the rapidly evolving landscape of financial services, where + precision and speed are paramount, the challenges associated with + extracting data from bank statements can become significant + roadblocks. Efficiently parsing financial information is critical + for accurately assessing financial health and risk. +

    +
    +
    + {[ + { + icon: FaSpinner, + title: "Slow & Labor-Intensive", + desc: "Teams spend countless hours manually keying in data, causing operational bottlenecks.", + }, + { + icon: FaExclamationTriangle, + title: "Prone to Human Error", + desc: "Manual entry risks inaccurate assessments, compliance breaches, and poor decisions.", + }, + { + icon: FaFileInvoiceDollar, + title: "High Operational Costs", + desc: "Significant resources consumed managing large volumes of statements manually.", + }, + { + icon: FaShieldAlt, + title: "Limited Fraud Detection", + desc: "Identifying sophisticated fraud or tampered documents manually is difficult and slow.", + }, + { + icon: FaTachometerAlt, + title: "Long Turnaround Times", + desc: "Slow processing delays approvals, causing customer dissatisfaction and missed opportunities.", + }, + { + icon: FaLayerGroup, + title: "Scalability Challenges", + desc: "Manual workflows bottleneck quickly, making it difficult to handle growing statement volumes efficiently.", + }, + ].map((item) => ( +
    + +

    + {item.title} +

    +

    + {item.desc} +

    +
    + ))} +
    +
    +
    + + {/* 3. Introducing OBSE Section */} +
    +
    +
    +
    +

    + OBSE: Automated Accuracy, Speed, and Insight +

    +

    + Optical Bank Statement Extractor (OBSE) is an advanced OCR and + data aggregation platform designed specifically for the South + African financial landscape. It seamlessly extracts + comprehensive, accurate data from any bank statement (individual + or juristic, across all SA banks), regardless of format – + including PDFs, scanned images, stamped documents, and even + disorganized statements. +

    +

    + OBSE transforms raw bank statement data into actionable + financial intelligence in seconds, eliminating manual drudgery + and empowering your teams to make faster, smarter decisions. +

    +
    +
    + {/* Placeholder for UI Screenshot */} + OBSE Upload Interface +
    +
    +
    +
    +
    + + {/* 4. How OBSE Works Section */} +
    +
    +
    +

    + From PDF to Insights in Seconds +

    +

    + OBSE simplifies complex data extraction into a streamlined + workflow. +

    +
    + {/* Consider a visual flow diagram or stepped cards */} +
    + {[ + { + step: 1, + title: "Upload", + desc: "Easily upload single multi-month PDFs or separate monthly files.", + icon: FaSignInAlt, + }, + { + step: 2, + title: "Automated Extraction", + desc: "Powerful engine reads, structures, and validates data in seconds.", + icon: FaCogs, + }, + { + step: 3, + title: "Analysis & Fraud Checks", + desc: "Analyzes transactions, identifies income, flags fraud indicators.", + icon: FaSearchDollar, + }, + { + step: 4, + title: "Review & Interact", + desc: "User-friendly interface to review, recategorize, and view insights.", + icon: FaUserCheck, + }, + { + step: 5, + title: "Access & Integrate", + desc: "Export data (Excel, PDF) or integrate results via API.", + icon: FaLaptopCode, + }, + ].map((item) => ( +
    +
    + {item.step} +
    + +

    + {item.title} +

    +

    + {item.desc} +

    +
    + ))} +
    +
    +
    + + {/* 5. Key Features Section */} +
    +
    +
    +

    + Unlock Deeper Financial Understanding +

    +

    + Explore the powerful capabilities that drive OBSE's + performance. +

    +
    +
    + {keyFeatures.map((feature) => ( +
    + {feature.isComingSoon && ( + + Coming Soon + + )} + +

    + {feature.title} +

    +

    + {feature.description} +

    +
    + ))} +
    +
    +
    + + {/* 6. Core Benefits Section */} +
    +
    +
    +

    + The OBSE Advantage: Transform Your Operations +

    +

    + Experience tangible benefits that impact your bottom line and + efficiency. +

    +
    +
    + {coreBenefits.map((benefit) => ( +
    + +

    + {benefit.title} +

    +

    + {benefit.description} +

    +
    + ))} +
    +
    +
    + + {/* 7. Data Outputs & Insights Section */} +
    +
    +
    +
    + {/* Placeholder for Dashboard Example */} + OBSE Dashboard Insights +
    +
    +

    + Actionable Intelligence at Your Fingertips +

    +

    + OBSE delivers a suite of standard financial summaries and + detailed data points essential for informed decisions: +

    +
      + {dataOutputs.map((output) => ( +
    • + + + {output} + +
    • + ))} +
    +
    +
    +
    +
    + + {/* 8. Technology Deep Dive Section */} +
    +
    +
    + +

    + Leveraging Advanced Technology for Reliable Results +

    +

    + Built on a foundation of cutting-edge technology to ensure + performance and accuracy. +

    +
    +
    + {[ + { + title: "Advanced OCR Engine", + desc: "Fine-tuned for diverse SA bank statements, handling stamps and challenging inputs.", + icon: FaFileAlt, + }, + { + title: "Machine Learning", + desc: "Continuously improves interpretation, context analysis, and fraud detection models.", + icon: FaLightbulb, + }, + { + title: "Data Structuring & Validation", + desc: "Intelligently structures data, validates entries, and applies rules for consistency.", + icon: FaPuzzlePiece, + }, + { + title: "Secure API", + desc: "Robust and secure API for seamless and safe integration with your existing systems.", + icon: FaShieldAlt, + }, + ].map((item) => ( +
    + +

    + {item.title} +

    +

    {item.desc}

    +
    + ))} +
    + {/* Optional: Add JSON snippet visual */} + {/*
    +
    
    +              {`{
    +  "transaction_id": "T12345",
    +  "date": "2023-10-26",
    +  "description": "SALARY ABC CORP",
    +  "amount": 15000.00,
    +  "type": "credit",
    +  "category": "Income/Salary"
    +}`}
    +            
    +
    */} +
    +
    + + {/* 9. Use Cases & Applications Section */} +
    +
    +
    + +

    + Empowering Various Financial Processes +

    +

    + OBSE provides critical data and automation for a wide range of + applications. +

    +
    +
    + {useCases.map((useCase) => ( +
    +

    + {useCase} +

    +
    + ))} +
    +
    +
    + + {/* 10. Customization & Partnership Section */} +
    +
    + +

    + Flexible Solutions Tailored to Your Needs +

    +

    + Beyond our standard offerings, we understand unique needs. If you + require specific information or analysis not in our standard + package, we'll collaborate to develop tailor-made outputs + aligned with your requirements. Our goal is to ensure OBSE exceeds + your expectations. +

    +

    + We offer Proof of Concept (POC) engagements to demonstrate + OBSE's capabilities with your data and workflows, allowing you + to evaluate its potential impact before full commitment. +

    +
    +
    + + {/* 11. About OMS Section (Brief) */} +
    +
    + {/* Optional: OMS Logo */} +

    + Your Experienced Partner in Financial Technology +

    +

    + Owethu Management Solutions (OMS) provides professional technology + services with a proven track record, including participation in + ABSA's supplier development program since 2020. OBSE is backed + by a dedicated team committed to delivering robust, user-friendly + solutions that solve real-world business challenges. +

    + + Learn More About OMS + +
    +
    + + {/* 12. Call to Action (CTA) Section */} +
    +
    +

    + Ready to Transform Your Bank Statement Processing? +

    +

    + See OBSE in action! Schedule a live demo tailored to your specific + use case and discover how OBSE can streamline your operations, + reduce costs, and mitigate risk. +

    +
    + + Request a Personalized Demo + + + Contact Sales Team + +
    + {/* Optional: Download Brochure Link */} + {/* */} +
    +
    + + {/* 13. Contact Information Section */} +
    +
    +
    +

    + Get in Touch +

    +

    + For personalized support or to schedule a demo, reach out to us. + Our team is here to help you harness the power of automated data + extraction. Take the first step toward enhancing your efficiency + and driving your success! +

    +
    +
    + {/* Contact Details */} +
    +
    + +
    +

    + Phone +

    + + (012) 051 3281 + +
    +
    +
    + +
    +

    + Email +

    + + Zanelem@oms.africa + +
    +
    + {/* Optional: Link to main contact page */} + + Go to Full Contact Page + +
    + + {/* Optional: Simple Contact Form (if ContactForm component is not used/available) */} + {/*
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    */} + + {/* OR Use the ContactForm Component */} +
    +

    + Send a Quick Inquiry +

    + +
    +
    +
    +
    +
    + ); +} diff --git a/app/(website)/vacancies/page.tsx b/app/(website)/vacancies/page.tsx index cc61e4b..3bde9a2 100644 --- a/app/(website)/vacancies/page.tsx +++ b/app/(website)/vacancies/page.tsx @@ -27,9 +27,6 @@ export const metadata: Metadata = { }; */ -// Define gold color for consistency (if COLORS.primary is not '#e1c44a', adjust accordingly) -const goldColor = COLORS.primary || "#e1c44a"; // Use COLORS.primary or fallback - // --- VacancyCard Component (no changes needed here) --- interface VacancyCardProps { vacancy: Vacancy; @@ -57,7 +54,7 @@ function VacancyCard({ vacancy }: VacancyCardProps) { >

    @@ -67,7 +64,7 @@ function VacancyCard({ vacancy }: VacancyCardProps) {