Merge pull request #2 from OwethuManagedServices/05-05-2025/Add-Toyota-on-Trusted-by-Partners
05 05 2025/add toyota on trusted by partners
74
actions/apply.ts
Normal file
@ -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<typeof applicationSchema>;
|
||||
|
||||
interface ActionResult {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export async function submitApplication(
|
||||
formData: ApplicationFormData
|
||||
): Promise<ActionResult> {
|
||||
// 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!" };
|
||||
}
|
||||
@ -1,84 +1,116 @@
|
||||
// components/ClientLogosSection.tsx
|
||||
import React from "react";
|
||||
import Image from "next/image"; // For actual logos
|
||||
import { FaBuilding, FaCar, FaLaptopCode, FaUsers } from "react-icons/fa"; // For placeholders
|
||||
import Image from "next/image";
|
||||
|
||||
// Define structure for client data (adapt as needed)
|
||||
// Define structure for client data - focusing on logos now
|
||||
type Client = {
|
||||
name: string;
|
||||
logoUrl?: string; // URL to actual logo image
|
||||
icon?: React.ElementType; // Placeholder icon component
|
||||
logoUrl: string; // Expecting actual logo URLs now
|
||||
};
|
||||
|
||||
type ClientLogosSectionProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
clients: Client[];
|
||||
/** Control animation speed (lower number = faster). Default: 40s */
|
||||
speed?: number;
|
||||
/** Size of the square background container in pixels. Default: 120 */
|
||||
squareSize?: number;
|
||||
};
|
||||
|
||||
const ClientLogosSection: React.FC<ClientLogosSectionProps> = ({
|
||||
title,
|
||||
description,
|
||||
clients,
|
||||
clients = [], // Default to empty array
|
||||
speed = 50, //Default speed in seconds for one full cycle
|
||||
squareSize = 120, // Default size for the square container (e.g., 120px)
|
||||
}) => {
|
||||
// Need at least one client to render the marquee
|
||||
if (clients.length === 0) {
|
||||
return null; // Or render a placeholder message if preferred
|
||||
}
|
||||
|
||||
// Duplicate the clients for a seamless loop effect
|
||||
const extendedClients = [...clients, ...clients];
|
||||
|
||||
const squareDim = `${squareSize}px`; // Convert size to string for inline style
|
||||
const padding = Math.round(squareSize / 6); // Calculate padding based on size (adjust divisor as needed)
|
||||
const paddingDim = `${padding}px`;
|
||||
|
||||
return (
|
||||
// Use semantic background
|
||||
<section className="py-16 md:py-20 bg-background">
|
||||
<section className="py-16 md:py-20 bg-background overflow-hidden">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
{" "}
|
||||
{/* Use container */}
|
||||
{/* Use semantic foreground */}
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-12 text-foreground">
|
||||
{title}
|
||||
</h2>
|
||||
{/* TODO: Implement Auto-Sliding Panel (e.g., using Swiper.js or Embla Carousel) */}
|
||||
<div className="flex flex-wrap justify-center items-center gap-10 md:gap-16 opacity-70 dark:opacity-60">
|
||||
{" "}
|
||||
{/* Adjust opacity */}
|
||||
{clients.map((client, index) => (
|
||||
</div>
|
||||
|
||||
{/* Marquee container - group allows pausing animation on hover */}
|
||||
<div className="relative w-full overflow-hidden group">
|
||||
{/* Inner container that will animate */}
|
||||
<div
|
||||
className="flex flex-nowrap animate-marquee-continuous"
|
||||
style={{ animationDuration: `${speed}s` }}
|
||||
>
|
||||
{extendedClients.map((client, index) => (
|
||||
<div
|
||||
key={index}
|
||||
title={client.name}
|
||||
className="transition-opacity hover:opacity-100"
|
||||
key={`${client.name}-${index}`}
|
||||
className="flex-shrink-0 mx-12 md:mx-16 py-4"
|
||||
>
|
||||
{client.logoUrl ? (
|
||||
{/* Square Background Container */}
|
||||
<div
|
||||
title={client.name}
|
||||
className="
|
||||
relative flex items-center justify-center
|
||||
bg-muted/30 dark:bg-muted/20
|
||||
rounded-lg shadow-sm
|
||||
overflow-hidden
|
||||
grayscale hover:grayscale-0
|
||||
opacity-70 hover:opacity-100
|
||||
transition-all duration-300 ease-in-out
|
||||
cursor-pointer
|
||||
"
|
||||
style={{
|
||||
width: squareDim,
|
||||
height: squareDim,
|
||||
padding: paddingDim, // Add padding inside the square
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={client.logoUrl}
|
||||
alt={`${client.name} Logo`}
|
||||
width={120} // Adjust size as needed
|
||||
height={40} // Adjust size as needed
|
||||
objectFit="contain"
|
||||
className="dark:invert dark:filter" // Example: Invert logo colors in dark mode if needed
|
||||
layout="fill" // Let image fill the relative container
|
||||
objectFit="contain" // Maintain aspect ratio within the container
|
||||
/>
|
||||
) : client.icon ? (
|
||||
// Use semantic muted foreground for icons, primary on hover
|
||||
React.createElement(client.icon, {
|
||||
className:
|
||||
"text-5xl text-muted-foreground/80 hover:text-primary transition-colors",
|
||||
})
|
||||
) : (
|
||||
<span className="text-muted-foreground">{client.name}</span> // Fallback text
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{description && (
|
||||
<p className="mt-8 text-muted-foreground italic text-sm">
|
||||
|
||||
{/* Optional: Add fade effect at the edges */}
|
||||
<div className="absolute inset-y-0 left-0 w-16 md:w-24 bg-gradient-to-r from-background to-transparent pointer-events-none z-10"></div>
|
||||
<div className="absolute inset-y-0 right-0 w-16 md:w-24 bg-gradient-to-l from-background to-transparent pointer-events-none z-10"></div>
|
||||
</div>
|
||||
|
||||
{description && (
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<p className="mt-10 text-muted-foreground italic text-sm">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
// Example default data matching the original page (using placeholders)
|
||||
export const defaultClients: Client[] = [
|
||||
{ name: "Financial Services Client", icon: FaBuilding },
|
||||
{ name: "Automotive Client", icon: FaCar },
|
||||
{ name: "Tech Industry Client", icon: FaLaptopCode },
|
||||
{ name: "Generic Client 1", icon: FaUsers },
|
||||
{ name: "Generic Client 2", icon: FaBuilding },
|
||||
{ name: "ABSA", logoUrl: "/images/absa.png" },
|
||||
{ name: "SYBRIN", logoUrl: "/images/sybrin.svg" },
|
||||
{ name: "SASOL", logoUrl: "/images/sasol.png" },
|
||||
{ name: "JACARANDA", logoUrl: "/images/jacaranda.png" },
|
||||
{ name: "SALESFORCE", logoUrl: "/images/salesforce.png" },
|
||||
{ name: "BCX", logoUrl: "/images/bcx.png" },
|
||||
{ name: "TOYOTA", logoUrl: "/images/toyota-logo.png" },
|
||||
];
|
||||
|
||||
export default ClientLogosSection;
|
||||
|
||||
@ -16,32 +16,32 @@ import {
|
||||
FaProjectDiagram,
|
||||
} from "react-icons/fa";
|
||||
|
||||
const leadershipTeam = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Michael Shapiro",
|
||||
title: "Managing Director",
|
||||
imageUrl: "/images/profile1.jpg",
|
||||
bio: "A seasoned leader and innovator with over 25 years of experience, driving strategic growth and fostering a culture of excellence within OMS.",
|
||||
linkedinUrl: "#",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Gareth Corbishley",
|
||||
title: "Director",
|
||||
imageUrl: "/images/profile2.jpg",
|
||||
bio: "Expert in operational efficiency and technological implementation, ensuring seamless project delivery and client satisfaction.",
|
||||
linkedinUrl: "#",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Darryl Govender",
|
||||
title: "Director",
|
||||
imageUrl: "/images/profile3.jpg",
|
||||
bio: "Specializes in aligning cutting-edge technology solutions with complex business needs to unlock transformative results.",
|
||||
linkedinUrl: "#",
|
||||
},
|
||||
];
|
||||
// const leadershipTeam = [
|
||||
// {
|
||||
// id: 1,
|
||||
// name: "Michael Shapiro",
|
||||
// title: "Managing Director",
|
||||
// imageUrl: "/images/profile1.jpg",
|
||||
// bio: "A seasoned leader and innovator with over 25 years of experience, driving strategic growth and fostering a culture of excellence within OMS.",
|
||||
// linkedinUrl: "#",
|
||||
// },
|
||||
// {
|
||||
// id: 2,
|
||||
// name: "Gareth Corbishley",
|
||||
// title: "Director",
|
||||
// imageUrl: "/images/profile2.jpg",
|
||||
// bio: "Expert in operational efficiency and technological implementation, ensuring seamless project delivery and client satisfaction.",
|
||||
// linkedinUrl: "#",
|
||||
// },
|
||||
// {
|
||||
// id: 3,
|
||||
// name: "Darryl Govender",
|
||||
// title: "Director",
|
||||
// imageUrl: "/images/profile3.jpg",
|
||||
// bio: "Specializes in aligning cutting-edge technology solutions with complex business needs to unlock transformative results.",
|
||||
// linkedinUrl: "#",
|
||||
// },
|
||||
// ];
|
||||
|
||||
const coreValues = [
|
||||
{
|
||||
@ -131,13 +131,13 @@ const industryExpertise = [
|
||||
|
||||
export default function AboutUsPage() {
|
||||
return (
|
||||
<div className="bg-white text-gray-800 overflow-x-hidden">
|
||||
<div className="bg-white text-gray-800 overflow-x-hidden dark:bg-gray-900 dark:text-gray-200">
|
||||
{" "}
|
||||
{/* Prevent horizontal scroll */}
|
||||
{/* Prevent horizontal scroll & Add dark mode base */}
|
||||
{/* Section 1: Hero / Company Overview */}
|
||||
<section className="relative bg-gradient-to-r from-gray-800 via-gray-700 to-gray-800 text-white py-24 md:py-40">
|
||||
{/* Optional decorative background elements */}
|
||||
<div className="absolute inset-0 bg-black opacity-30"></div>
|
||||
<div className="absolute inset-0 bg-black opacity-30 dark:opacity-50"></div>
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center opacity-10"
|
||||
style={{ backgroundImage: "url('/path/to/abstract-tech-bg.jpg')" }}
|
||||
@ -150,7 +150,7 @@ export default function AboutUsPage() {
|
||||
>
|
||||
About Owethu Managed Services
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed text-gray-200 font-poppins">
|
||||
<p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed text-gray-200 dark:text-gray-300 font-poppins">
|
||||
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() {
|
||||
</div>
|
||||
</section>
|
||||
{/* Section 2: Our Genesis & Purpose */}
|
||||
<section className="py-16 md:py-24 bg-gray-50">
|
||||
<section className="py-16 md:py-24 bg-gray-50 dark:bg-gray-800">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 font-poppins text-gray-900 leading-tight">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 font-poppins text-gray-900 dark:text-white leading-tight">
|
||||
Forged in Innovation, <br className="hidden md:inline" /> Driven
|
||||
by Purpose
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 leading-relaxed mb-4 font-poppins">
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed mb-4 font-poppins">
|
||||
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.
|
||||
</p>
|
||||
<p className="text-md md:text-lg text-gray-700 leading-relaxed font-poppins">
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed font-poppins">
|
||||
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
|
||||
@ -187,7 +187,7 @@ export default function AboutUsPage() {
|
||||
<div className="relative h-64 md:h-80 rounded-lg overflow-hidden shadow-lg">
|
||||
{/* Replace with a relevant, high-quality image representing innovation or teamwork */}
|
||||
<Image
|
||||
src="/images/team-collaborative.png"
|
||||
src="/images/team.svg"
|
||||
alt="Team collaborating on innovative solutions"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
@ -198,12 +198,12 @@ export default function AboutUsPage() {
|
||||
</div>
|
||||
</section>
|
||||
{/* Section 3: Vision & Mission (Similar layout, refined look) */}
|
||||
<section className="py-16 md:py-24 bg-white">
|
||||
<section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6 text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white">
|
||||
Our North Star
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-600 mt-3 max-w-2xl mx-auto font-poppins">
|
||||
<p className="text-md md:text-lg text-gray-600 dark:text-gray-400 mt-3 max-w-2xl mx-auto font-poppins">
|
||||
Guiding our strategy, actions, and partnerships.
|
||||
</p>
|
||||
</div>
|
||||
@ -211,17 +211,17 @@ export default function AboutUsPage() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-10 md:gap-12">
|
||||
{/* Vision Card */}
|
||||
<div
|
||||
className="bg-gray-50 p-8 rounded-xl shadow-md hover:shadow-lg transition-shadow duration-300 border-t-4 border-gold-500 flex flex-col items-center text-center"
|
||||
className="bg-gray-50 dark:bg-gray-800 p-8 rounded-xl shadow-md hover:shadow-lg transition-shadow duration-300 border-t-4 border-gold-500 flex flex-col items-center text-center"
|
||||
style={{ borderColor: "#e1c44a" }}
|
||||
>
|
||||
<FaBullseye
|
||||
className="text-5xl mb-5 text-gold-500"
|
||||
style={{ color: "#e1c44a" }}
|
||||
/>
|
||||
<h3 className="text-2xl font-bold mb-4 font-poppins text-gray-900">
|
||||
<h3 className="text-2xl font-bold mb-4 font-poppins text-gray-900 dark:text-white">
|
||||
Our Vision
|
||||
</h3>
|
||||
<p className="text-gray-700 leading-relaxed font-poppins flex-grow">
|
||||
<p className="text-gray-700 dark:text-gray-300 leading-relaxed font-poppins flex-grow">
|
||||
"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() {
|
||||
</div>
|
||||
{/* Mission Card */}
|
||||
<div
|
||||
className="bg-gray-50 p-8 rounded-xl shadow-md hover:shadow-lg transition-shadow duration-300 border-t-4 border-gold-500 flex flex-col items-center text-center"
|
||||
className="bg-gray-50 dark:bg-gray-800 p-8 rounded-xl shadow-md hover:shadow-lg transition-shadow duration-300 border-t-4 border-gold-500 flex flex-col items-center text-center"
|
||||
style={{ borderColor: "#e1c44a" }}
|
||||
>
|
||||
<FaHandshake
|
||||
className="text-5xl mb-5 text-gold-500"
|
||||
style={{ color: "#e1c44a" }}
|
||||
/>
|
||||
<h3 className="text-2xl font-bold mb-4 font-poppins text-gray-900">
|
||||
<h3 className="text-2xl font-bold mb-4 font-poppins text-gray-900 dark:text-white">
|
||||
Our Mission
|
||||
</h3>
|
||||
<p className="text-gray-700 leading-relaxed font-poppins flex-grow">
|
||||
<p className="text-gray-700 dark:text-gray-300 leading-relaxed font-poppins flex-grow">
|
||||
"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() {
|
||||
</div>
|
||||
</section>
|
||||
{/* Section 4: Our Approach & Methodology */}
|
||||
<section className="py-16 md:py-24 bg-gradient-to-br from-gray-100 to-gray-200">
|
||||
<section className="py-16 md:py-24 bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-700">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-14">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 mb-4">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
How We Deliver Excellence
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 max-w-3xl mx-auto font-poppins">
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto font-poppins">
|
||||
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) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="bg-white p-6 rounded-lg shadow-md text-center transform transition duration-300 hover:-translate-y-2 hover:shadow-xl"
|
||||
className="bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md text-center transform transition duration-300 hover:-translate-y-2 hover:shadow-xl dark:hover:shadow-gold-500/20"
|
||||
>
|
||||
<item.icon
|
||||
className="text-4xl text-gold-500 mx-auto mb-5"
|
||||
style={{ color: "#e1c44a" }}
|
||||
/>
|
||||
<h4 className="text-xl font-semibold mb-3 font-poppins">
|
||||
<h4 className="text-xl font-semibold mb-3 font-poppins dark:text-white">
|
||||
{item.title}
|
||||
</h4>
|
||||
<p className="text-gray-600 font-poppins text-sm leading-relaxed">
|
||||
<p className="text-gray-600 dark:text-gray-300 font-poppins text-sm leading-relaxed">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
@ -288,7 +288,7 @@ export default function AboutUsPage() {
|
||||
</div>
|
||||
</section>
|
||||
{/* Section 5: Industry Expertise */}
|
||||
<section className="py-16 md:py-24 bg-white">
|
||||
<section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12 items-center">
|
||||
<div className="lg:col-span-1">
|
||||
@ -296,10 +296,10 @@ export default function AboutUsPage() {
|
||||
className="text-5xl text-gold-500 mb-4"
|
||||
style={{ color: "#e1c44a" }}
|
||||
/>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4 font-poppins text-gray-900 leading-tight">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4 font-poppins text-gray-900 dark:text-white leading-tight">
|
||||
Deep Domain Knowledge
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 leading-relaxed font-poppins">
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed font-poppins">
|
||||
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) => (
|
||||
<div
|
||||
key={exp.industry}
|
||||
className="bg-gray-50 p-6 rounded-lg shadow-sm border-l-4 border-gold-500"
|
||||
className="bg-gray-50 dark:bg-gray-800 p-6 rounded-lg shadow-sm border-l-4 border-gold-500"
|
||||
style={{ borderColor: "#e1c44a" }}
|
||||
>
|
||||
<h4 className="text-lg font-semibold mb-2 font-poppins">
|
||||
<h4 className="text-lg font-semibold mb-2 font-poppins dark:text-white">
|
||||
{exp.industry}
|
||||
</h4>
|
||||
<p className="text-gray-600 font-poppins text-sm leading-relaxed">
|
||||
<p className="text-gray-600 dark:text-gray-300 font-poppins text-sm leading-relaxed">
|
||||
{exp.details}
|
||||
</p>
|
||||
</div>
|
||||
@ -331,7 +331,7 @@ export default function AboutUsPage() {
|
||||
className="text-5xl text-gold-500 mx-auto mb-5"
|
||||
style={{ color: "#e1c44a" }}
|
||||
/>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4 font-poppins">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4 font-poppins text-white">
|
||||
Smart Technology, Applied Wisely
|
||||
</h2>
|
||||
<p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed text-gray-300 mb-8 font-poppins">
|
||||
@ -339,38 +339,38 @@ export default function AboutUsPage() {
|
||||
strategically. Our focus is on building solutions that are:
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-4xl mx-auto">
|
||||
<div className="bg-gray-700 p-6 rounded-lg">
|
||||
<div className="bg-gray-700 dark:bg-gray-700 p-6 rounded-lg">
|
||||
<h4
|
||||
className="text-xl font-semibold mb-2 font-poppins text-gold-500"
|
||||
style={{ color: "#e1c44a" }}
|
||||
>
|
||||
Scalable & Future-Proof
|
||||
</h4>
|
||||
<p className="text-gray-300 font-poppins text-sm">
|
||||
<p className="text-gray-300 dark:text-gray-300 font-poppins text-sm">
|
||||
Designed to grow with your business and adapt to future
|
||||
technological advancements.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-700 p-6 rounded-lg">
|
||||
<div className="bg-gray-700 dark:bg-gray-700 p-6 rounded-lg">
|
||||
<h4
|
||||
className="text-xl font-semibold mb-2 font-poppins text-gold-500"
|
||||
style={{ color: "#e1c44a" }}
|
||||
>
|
||||
Robust & Reliable
|
||||
</h4>
|
||||
<p className="text-gray-300 font-poppins text-sm">
|
||||
<p className="text-gray-300 dark:text-gray-300 font-poppins text-sm">
|
||||
Built with quality and security at the core, ensuring dependable
|
||||
performance.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-700 p-6 rounded-lg">
|
||||
<div className="bg-gray-700 dark:bg-gray-700 p-6 rounded-lg">
|
||||
<h4
|
||||
className="text-xl font-semibold mb-2 font-poppins text-gold-500"
|
||||
style={{ color: "#e1c44a" }}
|
||||
>
|
||||
Best-Fit Solutions
|
||||
</h4>
|
||||
<p className="text-gray-300 font-poppins text-sm">
|
||||
<p className="text-gray-300 dark:text-gray-300 font-poppins text-sm">
|
||||
Chosen based on your specific needs, budget, and existing
|
||||
infrastructure, not just the latest hype.
|
||||
</p>
|
||||
@ -379,25 +379,25 @@ export default function AboutUsPage() {
|
||||
</div>
|
||||
</section>
|
||||
{/* Section 7: Core Values (Reusing previous structure, adjusting background) */}
|
||||
<section className="py-16 md:py-24 bg-gray-100">
|
||||
<section className="py-16 md:py-24 bg-gray-100 dark:bg-gray-800">
|
||||
<div className="container mx-auto px-6">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-center mb-12 font-poppins text-gray-900">
|
||||
<h2 className="text-3xl md:text-4xl font-bold text-center mb-12 font-poppins text-gray-900 dark:text-white">
|
||||
Our Foundational Values
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{coreValues.map((value) => (
|
||||
<div
|
||||
key={value.title}
|
||||
className="bg-white p-6 rounded-lg shadow-sm text-center transition-shadow duration-300 hover:shadow-lg"
|
||||
className="bg-white dark:bg-gray-700 p-6 rounded-lg shadow-sm text-center transition-shadow duration-300 hover:shadow-lg dark:hover:shadow-gold-500/20"
|
||||
>
|
||||
<value.icon
|
||||
className="text-4xl text-gold-500 mx-auto mb-4"
|
||||
style={{ color: "#e1c44a" }}
|
||||
/>
|
||||
<h4 className="text-xl font-semibold mb-2 font-poppins">
|
||||
<h4 className="text-xl font-semibold mb-2 font-poppins dark:text-white">
|
||||
{value.title}
|
||||
</h4>
|
||||
<p className="text-gray-600 font-poppins text-sm leading-relaxed">
|
||||
<p className="text-gray-600 dark:text-gray-300 font-poppins text-sm leading-relaxed">
|
||||
{value.description}
|
||||
</p>
|
||||
</div>
|
||||
@ -406,13 +406,13 @@ export default function AboutUsPage() {
|
||||
</div>
|
||||
</section>
|
||||
{/* Section 8: The OMS Partnership */}
|
||||
<section className="py-16 md:py-24 bg-white">
|
||||
<section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
||||
<div className="relative h-64 md:h-80 rounded-lg overflow-hidden shadow-lg order-last md:order-first">
|
||||
{/* Replace with an image representing partnership or client collaboration */}
|
||||
<Image
|
||||
src="/images/partners.jpg"
|
||||
src="/images/meeting.svg"
|
||||
alt="Client partnership and collaboration"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
@ -424,17 +424,17 @@ export default function AboutUsPage() {
|
||||
className="text-5xl text-gold-500 mb-4"
|
||||
style={{ color: "#e1c44a" }}
|
||||
/>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 font-poppins text-gray-900 leading-tight">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 font-poppins text-gray-900 dark:text-white leading-tight">
|
||||
More Than Vendors, <br className="hidden md:inline" />{" "}
|
||||
We're Partners
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 leading-relaxed mb-4 font-poppins">
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed mb-4 font-poppins">
|
||||
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.
|
||||
</p>
|
||||
<p className="text-md md:text-lg text-gray-700 leading-relaxed font-poppins">
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed font-poppins">
|
||||
This means open communication, shared goals, proactive
|
||||
problem-solving, and a long-term commitment to your success,
|
||||
extending far beyond project completion.
|
||||
@ -443,14 +443,14 @@ export default function AboutUsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/* Section 9: Our Leadership Team */}
|
||||
<section className="py-16 md:py-24 bg-gray-50">
|
||||
{/* Section 9: Our Leadership Team
|
||||
<section className="py-16 md:py-24 bg-gray-50 dark:bg-gray-800">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-14">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 mb-4">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Meet Our Leadership
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 max-w-3xl mx-auto font-poppins">
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto font-poppins">
|
||||
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
|
||||
@ -459,25 +459,22 @@ export default function AboutUsPage() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-10 justify-center">
|
||||
{/* Added justify-center */}
|
||||
{leadershipTeam.map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className="bg-white rounded-lg shadow-lg overflow-hidden text-center transition-transform duration-300 hover:scale-105 max-w-sm mx-auto"
|
||||
className="bg-white dark:bg-gray-700 rounded-lg shadow-lg overflow-hidden text-center transition-transform duration-300 hover:scale-105 max-w-sm mx-auto dark:hover:shadow-gold-500/20"
|
||||
>
|
||||
{/* Added max-width and mx-auto for centering if fewer than 3 items */}
|
||||
<div className="relative h-60 w-full">
|
||||
{/* Slightly taller image */}
|
||||
<Image
|
||||
src={member.imageUrl || "/images/profile1.jpg"}
|
||||
alt={`Photo of ${member.name}`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="bg-gray-200"
|
||||
className="bg-gray-200 dark:bg-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<h4 className="text-xl font-semibold font-poppins mb-1">
|
||||
<h4 className="text-xl font-semibold font-poppins mb-1 dark:text-white">
|
||||
{member.name}
|
||||
</h4>
|
||||
<p
|
||||
@ -486,7 +483,7 @@ export default function AboutUsPage() {
|
||||
>
|
||||
{member.title}
|
||||
</p>
|
||||
<p className="text-gray-600 font-poppins text-sm mb-4">
|
||||
<p className="text-gray-600 dark:text-gray-300 font-poppins text-sm mb-4">
|
||||
{member.bio}
|
||||
</p>
|
||||
{member.linkedinUrl && member.linkedinUrl !== "#" && (
|
||||
@ -494,10 +491,9 @@ 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 */}
|
||||
Connect on LinkedIn
|
||||
</a>
|
||||
)}
|
||||
@ -505,20 +501,20 @@ export default function AboutUsPage() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Button removed as requested */}
|
||||
</div>
|
||||
</section>
|
||||
*/}
|
||||
{/* Section 10: Commitment to Impact (Optional, but adds value) */}
|
||||
<section
|
||||
className="py-16 md:py-24 bg-gold-500 text-gray-900"
|
||||
className="py-16 md:py-24 bg-gold-500 text-gray-900 dark:bg-gold-600 dark:text-gray-900"
|
||||
style={{ backgroundColor: "#e1c44a" }}
|
||||
>
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
<FaChartLine className="text-5xl mx-auto mb-5" />
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4 font-poppins">
|
||||
<FaChartLine className="text-5xl mx-auto mb-5 text-gray-800" />
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4 font-poppins text-gray-900">
|
||||
Driving Measurable Impact
|
||||
</h2>
|
||||
<p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed font-poppins">
|
||||
<p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed font-poppins text-gray-800">
|
||||
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 +525,7 @@ export default function AboutUsPage() {
|
||||
<div className="mt-10">
|
||||
<a
|
||||
href="/contact"
|
||||
className="inline-block bg-gray-800 text-white font-bold py-3 px-8 rounded-md hover:bg-gray-900 transition-colors duration-300 font-poppins"
|
||||
className="inline-block bg-gray-800 text-white font-bold py-3 px-8 rounded-md hover:bg-gray-900 dark:bg-gray-900 dark:hover:bg-black transition-colors duration-300 font-poppins"
|
||||
>
|
||||
Start the Conversation
|
||||
</a>
|
||||
|
||||
@ -1,48 +1,73 @@
|
||||
import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import ContactForm from "@/components/ContactForm";
|
||||
import { FiMapPin, FiPhone, FiMail, FiClock } from "react-icons/fi"; // Import icons
|
||||
"use client"; // Needed for FAQ state
|
||||
|
||||
// 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",
|
||||
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";
|
||||
|
||||
// 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.",
|
||||
},
|
||||
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",
|
||||
},
|
||||
],
|
||||
{
|
||||
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.",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Contact OMS",
|
||||
description: "Get in touch with Owethu Managed Services.",
|
||||
images: ["/og-image-contact.jpg"],
|
||||
{
|
||||
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<number | null>(null);
|
||||
|
||||
const toggleFaq = (id: number) => {
|
||||
setOpenFaqId(openFaqId === id ? null : id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-b from-background to-secondary/50 text-foreground">
|
||||
{/* Hero Section */}
|
||||
<section className="py-20 md:py-28 text-center bg-primary/10 dark:bg-primary/5 border-b border-border">
|
||||
<div className="container mx-auto px-4">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4 text-primary">
|
||||
// Added dark mode base styles
|
||||
<div className="bg-gray-50 text-gray-800 dark:bg-gray-900 dark:text-gray-200 overflow-x-hidden font-poppins">
|
||||
{/* Hero Section - Adjusted padding for mobile */}
|
||||
<section className="relative bg-gradient-to-r from-gray-800 via-gray-700 to-gray-800 text-white py-16 md:py-28">
|
||||
<div className="absolute inset-0 bg-black opacity-40 dark:opacity-50"></div>
|
||||
<div className="container mx-auto px-4 md:px-6 text-center relative z-10">
|
||||
<h1
|
||||
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold mb-4 font-poppins drop-shadow-md dark:text-gold-400" // Use primary color directly or a dark-mode friendly version
|
||||
style={{ color: COLORS.primary }} // Keep gold color for heading, ensure contrast in dark mode
|
||||
>
|
||||
Get In Touch
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
<p className="text-base sm:text-lg md:text-xl max-w-3xl mx-auto leading-relaxed text-gray-200 dark:text-gray-300 font-poppins">
|
||||
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 +75,31 @@ export default function ContactPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Content Area (Form + Info) */}
|
||||
<section className="py-16 md:py-24">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 md:gap-16 lg:gap-20 items-start">
|
||||
{/* Contact Information Section */}
|
||||
<div className="space-y-8 bg-card p-8 rounded-lg border border-border shadow-sm">
|
||||
<h2 className="text-3xl font-semibold mb-6 text-foreground">
|
||||
{/* Main Content Area (Form + Info) - Adjusted padding, added dark mode */}
|
||||
<section className="py-12 md:py-24 bg-white dark:bg-gray-800">
|
||||
<div className="container mx-auto px-4 md:px-6">
|
||||
{/* Grid stacks vertically by default, becomes 2 columns on large screens */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 md:gap-16 items-start">
|
||||
{/* Contact Information Section - Added dark mode styles */}
|
||||
<div
|
||||
className="bg-gray-50 dark:bg-gray-700 p-6 md:p-8 rounded-xl shadow-md border-t-4 border-gold-500 dark:border-gold-400 space-y-6 md:space-y-8"
|
||||
style={{ borderColor: COLORS.primary }} // Keep primary border color
|
||||
>
|
||||
<h2 className="text-2xl md:text-3xl font-bold font-poppins text-gray-900 dark:text-white mb-6">
|
||||
Contact Information
|
||||
</h2>
|
||||
|
||||
<div className="flex items-start space-x-4">
|
||||
<FiMapPin className="w-6 h-6 text-primary mt-1 flex-shrink-0" />
|
||||
<FaMapMarkerAlt
|
||||
className="w-6 h-6 text-gold-500 mt-1 flex-shrink-0"
|
||||
style={{ color: COLORS.primary }} // Keep primary icon color
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-foreground">
|
||||
<h3 className="text-lg font-semibold font-poppins text-gray-800 dark:text-gray-100">
|
||||
Our Office
|
||||
</h3>
|
||||
<address className="text-muted-foreground not-italic text-sm leading-relaxed">
|
||||
<address className="text-gray-600 dark:text-gray-300 font-poppins not-italic text-sm leading-relaxed">
|
||||
{/* ... existing address lines ... */}
|
||||
Unit 10 B Centuria Park
|
||||
<br />
|
||||
265 Von Willich Avenue
|
||||
@ -75,12 +108,12 @@ export default function ContactPage() {
|
||||
<br />
|
||||
South Africa
|
||||
</address>
|
||||
{/* Optional: Link to Google Maps */}
|
||||
<a
|
||||
href="https://www.google.com/maps/place/Owethu+Managed+Services/@-25.863168,28.186075,17z/data=!3m1!4b1!4m6!3m5!1s0x1e9565b7018f5f9b:0x4d8a4ae3a2c0d9a1!8m2!3d-25.8631728!4d28.1886499!16s%2Fg%2F11h1_q_1_f?entry=ttu" // Replace with your actual Google Maps link
|
||||
href="https://www.google.com/maps/place/Owethu+Managed+Services/@-25.863168,28.186075,17z/data=!3m1!4b1!4m6!3m5!1s0x1e9565b7018f5f9b:0x4d8a4ae3a2c0d9a1!8m2!3d-25.8631728!4d28.1886499!16s%2Fg%2F11h1_q_1_f?entry=ttu"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-primary hover:underline mt-2 inline-block"
|
||||
className="text-sm font-medium font-poppins hover:underline mt-2 inline-block dark:text-gold-400 dark:hover:text-gold-300"
|
||||
style={{ color: COLORS.primary }} // Keep primary link color
|
||||
>
|
||||
View on Google Maps
|
||||
</a>
|
||||
@ -88,12 +121,17 @@ export default function ContactPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-4">
|
||||
<FiPhone className="w-6 h-6 text-primary mt-1 flex-shrink-0" />
|
||||
<FaPhone
|
||||
className="w-5 h-5 text-gold-500 mt-1 flex-shrink-0"
|
||||
style={{ color: COLORS.primary }} // Keep primary icon color
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-foreground">Phone</h3>
|
||||
<h3 className="text-lg font-semibold font-poppins text-gray-800 dark:text-gray-100">
|
||||
Phone
|
||||
</h3>
|
||||
<a
|
||||
href="tel:+27120513282"
|
||||
className="text-muted-foreground hover:text-primary transition text-sm"
|
||||
className="text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100 transition text-sm font-poppins"
|
||||
>
|
||||
(012) 051 3282
|
||||
</a>
|
||||
@ -101,12 +139,17 @@ export default function ContactPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-4">
|
||||
<FiMail className="w-6 h-6 text-primary mt-1 flex-shrink-0" />
|
||||
<FaEnvelope
|
||||
className="w-5 h-5 text-gold-500 mt-1 flex-shrink-0"
|
||||
style={{ color: COLORS.primary }} // Keep primary icon color
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-foreground">Email</h3>
|
||||
<h3 className="text-lg font-semibold font-poppins text-gray-800 dark:text-gray-100">
|
||||
Email
|
||||
</h3>
|
||||
<a
|
||||
href="mailto:hello@oms.africa"
|
||||
className="text-muted-foreground hover:text-primary transition text-sm"
|
||||
className="text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100 transition text-sm font-poppins"
|
||||
>
|
||||
hello@oms.africa
|
||||
</a>
|
||||
@ -114,38 +157,109 @@ export default function ContactPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-4">
|
||||
<FiClock className="w-6 h-6 text-primary mt-1 flex-shrink-0" />
|
||||
<FaClock
|
||||
className="w-5 h-5 text-gold-500 mt-1 flex-shrink-0"
|
||||
style={{ color: COLORS.primary }} // Keep primary icon color
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-foreground">
|
||||
<h3 className="text-lg font-semibold font-poppins text-gray-800 dark:text-gray-100">
|
||||
Business Hours
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm font-poppins">
|
||||
Monday - Friday: 8:00 AM - 5:00 PM (SAST)
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm font-poppins">
|
||||
Weekends & Public Holidays: Closed
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Form Section */}
|
||||
<div className="bg-card p-8 rounded-lg border border-border shadow-sm">
|
||||
<h2 className="text-3xl font-semibold mb-6 text-foreground">
|
||||
{/* Contact Form Section - Added dark mode styles */}
|
||||
<div
|
||||
className="bg-gray-50 dark:bg-gray-700 p-6 md:p-8 rounded-xl shadow-md border-t-4 border-gold-500 dark:border-gold-400"
|
||||
style={{ borderColor: COLORS.primary }} // Keep primary border color
|
||||
>
|
||||
<h2 className="text-2xl md:text-3xl font-bold font-poppins text-gray-900 dark:text-white mb-6">
|
||||
Send Us a Message
|
||||
</h2>
|
||||
{/* Assuming ContactForm handles its own dark mode or inherits text colors */}
|
||||
<ContactForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Optional: Map Section Placeholder */}
|
||||
{/* <section className="h-96 bg-muted border-t border-border">
|
||||
<div className="container mx-auto h-full flex items-center justify-center">
|
||||
<p className="text-muted-foreground">[Embedded Map Placeholder - e.g., Google Maps iframe]</p>
|
||||
</div>
|
||||
</section> */}
|
||||
{/* Map Section - Adjusted padding, added dark mode, responsive height */}
|
||||
<section className="py-12 md:py-24 bg-gray-100 dark:bg-gray-900">
|
||||
<div className="container mx-auto px-4 md:px-6">
|
||||
<h2 className="text-2xl md:text-3xl lg:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-8 text-center">
|
||||
Find Us Here
|
||||
</h2>
|
||||
{/* Adjusted height for different screen sizes */}
|
||||
<div className="aspect-w-16 aspect-h-9 h-64 sm:h-80 md:h-96 mx-auto rounded-lg overflow-hidden shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
<iframe
|
||||
// ... existing iframe attributes ...
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d1313.49241475678!2d28.192648350415848!3d-25.85177958049519!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x1e95643aa4e82f9f%3A0xe4052722532cd30f!2sCenturia%20Park!5e1!3m2!1sen!2sza!4v1745745054858!5m2!1sen!2sza"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ border: 0 }}
|
||||
allowFullScreen={true}
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
title="Owethu Managed Services Location"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section - Adjusted padding, added dark mode */}
|
||||
<section className="py-12 md:py-24 bg-white dark:bg-gray-800">
|
||||
<div className="container mx-auto px-4 md:px-6">
|
||||
<h2 className="text-2xl md:text-3xl lg:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-10 md:mb-12 text-center">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
<div className="max-w-5xl mx-auto space-y-4">
|
||||
{faqData.map((faq) => (
|
||||
<div
|
||||
key={faq.id}
|
||||
className="border border-gray-200 dark:border-gray-600 rounded-lg overflow-hidden shadow-sm bg-white dark:bg-gray-700"
|
||||
>
|
||||
<button
|
||||
onClick={() => toggleFaq(faq.id)}
|
||||
className="flex justify-between items-center w-full p-4 text-left font-semibold font-poppins text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gold-500 dark:focus:ring-gold-400 focus:ring-opacity-50"
|
||||
aria-expanded={openFaqId === faq.id}
|
||||
aria-controls={`faq-answer-${faq.id}`}
|
||||
>
|
||||
<span className="text-sm md:text-base">{faq.question}</span>
|
||||
{openFaqId === faq.id ? (
|
||||
<FaChevronUp
|
||||
className="w-5 h-5 text-gold-500 flex-shrink-0 ml-2" // Added flex-shrink-0 and margin
|
||||
style={{ color: COLORS.primary }} // Keep primary icon color
|
||||
/>
|
||||
) : (
|
||||
<FaChevronDown
|
||||
className="w-5 h-5 text-gold-500 flex-shrink-0 ml-2" // Added flex-shrink-0 and margin
|
||||
style={{ color: COLORS.primary }} // Keep primary icon color
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
<div
|
||||
id={`faq-answer-${faq.id}`}
|
||||
// Adjusted padding and text size
|
||||
className={`px-4 pb-4 pt-2 text-gray-600 dark:text-gray-300 font-poppins text-xs sm:text-sm leading-relaxed transition-all duration-300 ease-in-out ${
|
||||
openFaqId === faq.id ? "block" : "hidden"
|
||||
}`}
|
||||
role="region"
|
||||
aria-labelledby={`faq-question-${faq.id}`}
|
||||
>
|
||||
{faq.answer}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
959
app/(website)/obse/page-backup.tsx
Normal file
@ -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 (
|
||||
// <>
|
||||
// <section className="relative bg-gradient-to-r from-gray-800 via-gray-700 to-gray-900 text-white py-24 md:py-40">
|
||||
// <div className="absolute inset-0 bg-black opacity-40 dark:opacity-60"></div>
|
||||
// <div className="absolute inset-0 bg-cover bg-center opacity-10 dark:opacity-5"></div>
|
||||
// <div className="container mx-auto px-6 text-center relative z-10">
|
||||
// <div className="mb-6 inline-block bg-gold-500/10 p-3 rounded-lg border border-gold-500/30">
|
||||
// <Image
|
||||
// src="/images/oms-logo-light.png"
|
||||
// alt="OMS Logo"
|
||||
// width={80}
|
||||
// height={40}
|
||||
// className="h-10 w-auto"
|
||||
// />
|
||||
// </div>
|
||||
// <h1 className="text-4xl md:text-6xl font-bold mb-4 font-poppins text-gold-500 drop-shadow-md">
|
||||
// Revolutionize Lending with OBSE
|
||||
// </h1>
|
||||
// <p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed text-gray-200 dark:text-gray-300 font-poppins mb-8">
|
||||
// Automate data extraction, enhance accuracy, detect fraud, and
|
||||
// accelerate decision-making with South Africa's intelligent bank
|
||||
// statement solution: Owethu Bank Statement Extraction.
|
||||
// </p>
|
||||
// <div className="flex flex-col sm:flex-row justify-center gap-4">
|
||||
// <Link
|
||||
// href="#contact"
|
||||
// className="inline-block bg-gold-500 text-gray-900 font-bold py-3 px-8 rounded-md hover:bg-gold-600 dark:hover:bg-gold-400 transition-colors duration-300 font-poppins"
|
||||
// >
|
||||
// Request a Demo <FaArrowRight className="inline ml-2" />
|
||||
// </Link>
|
||||
// <Link
|
||||
// href="#how-it-works"
|
||||
// className="inline-block bg-transparent border-2 border-gold-500 text-gold-500 font-bold py-3 px-8 rounded-md hover:bg-gold-500/10 transition-colors duration-300 font-poppins"
|
||||
// >
|
||||
// <FaPlayCircle className="inline mr-2" /> See How It Works
|
||||
// </Link>
|
||||
// </div>
|
||||
// <p className="text-sm text-gray-400 mt-6 font-poppins">
|
||||
// Keywords: Bank Statement Extraction, OCR, Automation, Financial Data
|
||||
// Analysis, South Africa
|
||||
// </p>
|
||||
// </div>
|
||||
// </section>
|
||||
|
||||
// <section className="py-16 md:py-24 bg-gray-50 dark:bg-gray-800">
|
||||
// <div className="container mx-auto px-6">
|
||||
// <div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
||||
// <div>
|
||||
// <FaExclamationTriangle
|
||||
// className="text-5xl text-gold-500 mb-4"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h2 className="text-3xl md:text-4xl font-bold mb-6 font-poppins text-gray-900 dark:text-white leading-tight">
|
||||
// The High Cost of Manual Bank Statement Processing
|
||||
// </h2>
|
||||
// <p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed mb-6 font-poppins">
|
||||
// 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.
|
||||
// </p>
|
||||
// <ul className="space-y-4 font-poppins">
|
||||
// <li className="flex items-start">
|
||||
// <FaHourglassHalf
|
||||
// className="text-gold-500 mt-1 mr-3 flex-shrink-0"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <span className="text-gray-700 dark:text-gray-300">
|
||||
// <strong className="dark:text-white">
|
||||
// Slow & Labor-Intensive:
|
||||
// </strong>{" "}
|
||||
// Countless hours spent on manual data entry lead to
|
||||
// operational bottlenecks.
|
||||
// </span>
|
||||
// </li>
|
||||
// <li className="flex items-start">
|
||||
// <FaUserCheck className="text-red-500 mt-1 mr-3 flex-shrink-0" />{" "}
|
||||
// {/* Using red for error */}
|
||||
// <span className="text-gray-700 dark:text-gray-300">
|
||||
// <strong className="dark:text-white">
|
||||
// Prone to Human Error:
|
||||
// </strong>{" "}
|
||||
// Mistakes risk inaccurate assessments, compliance breaches,
|
||||
// and poor decisions.
|
||||
// </span>
|
||||
// </li>
|
||||
// <li className="flex items-start">
|
||||
// <FaHandHoldingUsd
|
||||
// className="text-gold-500 mt-1 mr-3 flex-shrink-0"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <span className="text-gray-700 dark:text-gray-300">
|
||||
// <strong className="dark:text-white">
|
||||
// High Operational Costs:
|
||||
// </strong>{" "}
|
||||
// Significant resources consumed managing large volumes
|
||||
// manually.
|
||||
// </span>
|
||||
// </li>
|
||||
// <li className="flex items-start">
|
||||
// <FaShieldAlt className="text-red-500 mt-1 mr-3 flex-shrink-0" />{" "}
|
||||
// {/* Using red for ineffective fraud detection */}
|
||||
// <span className="text-gray-700 dark:text-gray-300">
|
||||
// <strong className="dark:text-white">
|
||||
// Limited Fraud Detection:
|
||||
// </strong>{" "}
|
||||
// Manually identifying sophisticated fraud is difficult and
|
||||
// time-consuming.
|
||||
// </span>
|
||||
// </li>
|
||||
// <li className="flex items-start">
|
||||
// <FaBusinessTime
|
||||
// className="text-gold-500 mt-1 mr-3 flex-shrink-0"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <span className="text-gray-700 dark:text-gray-300">
|
||||
// <strong className="dark:text-white">
|
||||
// Long Turnaround Times:
|
||||
// </strong>{" "}
|
||||
// Slow processing leads to customer dissatisfaction and lost
|
||||
// opportunities.
|
||||
// </span>
|
||||
// </li>
|
||||
// </ul>
|
||||
// </div>
|
||||
// <div className="relative h-72 md:h-96 rounded-lg overflow-hidden shadow-lg">
|
||||
// <Image
|
||||
// src="/images/manual-vs-automated.png" // Placeholder: Contrast image
|
||||
// alt="Manual data entry challenges vs automated efficiency"
|
||||
// layout="fill"
|
||||
// objectFit="cover"
|
||||
// className="transition-transform duration-500 hover:scale-105"
|
||||
// />
|
||||
// <div className="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent"></div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </section>
|
||||
|
||||
// {/* --- 3. Introducing OBSE: The Intelligent Solution --- */}
|
||||
// <section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
// <div className="container mx-auto px-6">
|
||||
// <div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
||||
// <div className="relative h-72 md:h-96 rounded-lg overflow-hidden shadow-lg order-last md:order-first">
|
||||
// {/* Use the UI screenshot showing upload */}
|
||||
// <Image
|
||||
// src="/images/obse-ui-upload.png" // Placeholder: Catalogue Page 4 Left
|
||||
// alt="OBSE Bank Statement Upload Interface"
|
||||
// layout="fill"
|
||||
// objectFit="contain" // Use contain if you want to show the full UI without cropping
|
||||
// className="p-4 bg-gray-100 dark:bg-gray-800" // Added padding and background
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <FaLightbulb
|
||||
// className="text-5xl text-gold-500 mb-4"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h2 className="text-3xl md:text-4xl font-bold mb-6 font-poppins text-gray-900 dark:text-white leading-tight">
|
||||
// OBSE: Automated Accuracy, Speed, and Insight
|
||||
// </h2>
|
||||
// <p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed mb-4 font-poppins">
|
||||
// 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{" "}
|
||||
// <strong className="dark:text-white">
|
||||
// any SA bank statement
|
||||
// </strong>{" "}
|
||||
// (individual or juristic), regardless of format – including PDF
|
||||
// templates, scanned images, stamped documents, and even
|
||||
// disorganized statements.
|
||||
// </p>
|
||||
// <p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed font-poppins font-semibold text-gold-600 dark:text-gold-400">
|
||||
// OBSE transforms raw bank statement data into actionable
|
||||
// financial intelligence in seconds, eliminating manual drudgery
|
||||
// and empowering your teams to make faster, smarter decisions.
|
||||
// </p>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </section>
|
||||
|
||||
// {/* --- 4. How OBSE Works: Simple Steps, Powerful Results --- */}
|
||||
// <section
|
||||
// id="how-it-works"
|
||||
// className="py-16 md:py-24 bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-700"
|
||||
// >
|
||||
// <div className="container mx-auto px-6">
|
||||
// <div className="text-center mb-14">
|
||||
// <FaCogs
|
||||
// className="text-5xl text-gold-500 mx-auto mb-5"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
// From PDF to Insights in Seconds
|
||||
// </h2>
|
||||
// <p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto font-poppins">
|
||||
// OBSE simplifies complex data extraction into a streamlined
|
||||
// workflow:
|
||||
// </p>
|
||||
// </div>
|
||||
// {/* Process Steps - Can use a grid or timeline structure */}
|
||||
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-8 text-center">
|
||||
// {/* Step 1: Upload */}
|
||||
// <div className="flex flex-col items-center p-4">
|
||||
// <div className="bg-gold-500/20 p-4 rounded-full mb-4 border border-gold-500/50">
|
||||
// <FaUpload
|
||||
// className="text-3xl text-gold-500"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// </div>
|
||||
// <h4 className="text-lg font-semibold mb-2 font-poppins dark:text-white">
|
||||
// 1. Upload
|
||||
// </h4>
|
||||
// <p className="text-gray-600 dark:text-gray-300 font-poppins text-sm">
|
||||
// Easily upload single or multiple bank statements (PDFs).
|
||||
// </p>
|
||||
// </div>
|
||||
// {/* Step 2: Extract */}
|
||||
// <div className="flex flex-col items-center p-4">
|
||||
// <div className="bg-gold-500/20 p-4 rounded-full mb-4 border border-gold-500/50">
|
||||
// <FaBolt
|
||||
// className="text-3xl text-gold-500"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// </div>
|
||||
// <h4 className="text-lg font-semibold mb-2 font-poppins dark:text-white">
|
||||
// 2. Automated Extraction
|
||||
// </h4>
|
||||
// <p className="text-gray-600 dark:text-gray-300 font-poppins text-sm">
|
||||
// Powerful engine reads, structures, and validates data in seconds
|
||||
// </p>
|
||||
// </div>
|
||||
// {/* Step 3: Analyze & Check */}
|
||||
// <div className="flex flex-col items-center p-4">
|
||||
// <div className="bg-gold-500/20 p-4 rounded-full mb-4 border border-gold-500/50">
|
||||
// <FaShieldAlt
|
||||
// className="text-3xl text-gold-500"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// </div>
|
||||
// <h4 className="text-lg font-semibold mb-2 font-poppins dark:text-white">
|
||||
// 3. Analysis & Fraud Check
|
||||
// </h4>
|
||||
// <p className="text-gray-600 dark:text-gray-300 font-poppins text-sm">
|
||||
// Analyzes transactions, identifies income, flags potential fraud.
|
||||
// </p>
|
||||
// </div>
|
||||
// {/* Step 4: Review */}
|
||||
// <div className="flex flex-col items-center p-4">
|
||||
// <div className="bg-gold-500/20 p-4 rounded-full mb-4 border border-gold-500/50">
|
||||
// <FaEye
|
||||
// className="text-3xl text-gold-500"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// </div>
|
||||
// <h4 className="text-lg font-semibold mb-2 font-poppins dark:text-white">
|
||||
// 4. Review & Interact (Optional)
|
||||
// </h4>
|
||||
// <p className="text-gray-600 dark:text-gray-300 font-poppins text-sm">
|
||||
// User-friendly interface to review, recategorize (real-time), and
|
||||
// view insights.
|
||||
// </p>
|
||||
// </div>
|
||||
// {/* Step 5: Access/Integrate */}
|
||||
// <div className="flex flex-col items-center p-4">
|
||||
// <div className="bg-gold-500/20 p-4 rounded-full mb-4 border border-gold-500/50">
|
||||
// <FaPaperPlane
|
||||
// className="text-3xl text-gold-500"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// </div>
|
||||
// <h4 className="text-lg font-semibold mb-2 font-poppins dark:text-white">
|
||||
// 5. Access & Integrate
|
||||
// </h4>
|
||||
// <p className="text-gray-600 dark:text-gray-300 font-poppins text-sm">
|
||||
// Export (Excel, PDF), use visual analytics, or integrate via API.
|
||||
// </p>
|
||||
// </div>
|
||||
// </div>
|
||||
// {/* Visual for the process flow */}
|
||||
// <div className="mt-12 relative h-60 md:h-96 rounded-lg overflow-hidden shadow-lg bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
|
||||
// <Image
|
||||
// src="/images/obse-process-flow.png" // Placeholder: Sequence of UI screenshots from Page 6
|
||||
// alt="OBSE Process Flow Visualization"
|
||||
// layout="fill"
|
||||
// objectFit="contain"
|
||||
// className="p-4"
|
||||
// />
|
||||
// <p className="absolute text-gray-500 dark:text-gray-400 z-0">
|
||||
// Process Flow Visual (Replace with Image)
|
||||
// </p>
|
||||
// </div>
|
||||
// </div>
|
||||
// </section>
|
||||
|
||||
// {/* --- 5. Key Features: Powering Your Processes --- */}
|
||||
// <section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
// <div className="container mx-auto px-6">
|
||||
// <div className="text-center mb-14">
|
||||
// {/* <FaListCheck
|
||||
// className="text-5xl text-gold-500 mx-auto mb-5"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// /> */}
|
||||
// <h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
// Unlock Deeper Financial Understanding
|
||||
// </h2>
|
||||
// <p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto font-poppins">
|
||||
// Explore the core capabilities that make OBSE an indispensable tool
|
||||
// for modern financial analysis.
|
||||
// </p>
|
||||
// </div>
|
||||
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
// {keyFeatures.map((feature) => (
|
||||
// <div
|
||||
// key={feature.title}
|
||||
// className="bg-gray-50 dark:bg-gray-800 p-6 rounded-lg shadow-md transform transition duration-300 hover:-translate-y-2 hover:shadow-xl dark:hover:shadow-gold-500/20 border-t-4 border-gold-500"
|
||||
// style={{ borderColor: "#e1c44a" }}
|
||||
// >
|
||||
// <feature.icon
|
||||
// className="text-4xl text-gold-500 mb-4" // Adjusted size
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h4 className="text-xl font-semibold mb-3 font-poppins dark:text-white">
|
||||
// {feature.title}
|
||||
// </h4>
|
||||
// <p className="text-gray-600 dark:text-gray-300 font-poppins text-sm leading-relaxed">
|
||||
// {feature.description}
|
||||
// </p>
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
// </section>
|
||||
|
||||
// {/* --- 6. Core Benefits: The OBSE Advantage (The Gain) --- */}
|
||||
// <section className="py-16 md:py-24 bg-gray-100 dark:bg-gray-800">
|
||||
// <div className="container mx-auto px-6">
|
||||
// <div className="text-center mb-14">
|
||||
// <FaThumbsUp
|
||||
// className="text-5xl text-gold-500 mx-auto mb-5"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
// Transform Operations & Decision-Making
|
||||
// </h2>
|
||||
// <p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto font-poppins">
|
||||
// Discover the tangible benefits OBSE brings to your financial
|
||||
// workflows and bottom line.
|
||||
// </p>
|
||||
// </div>
|
||||
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
// {coreBenefits.map((benefit) => (
|
||||
// <div
|
||||
// key={benefit.title}
|
||||
// className="bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md text-center transform transition duration-300 hover:-translate-y-2 hover:shadow-xl dark:hover:shadow-gold-500/20"
|
||||
// >
|
||||
// <benefit.icon
|
||||
// className="text-4xl text-gold-500 mx-auto mb-4"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h4 className="text-xl font-semibold mb-2 font-poppins dark:text-white">
|
||||
// {benefit.title}
|
||||
// </h4>
|
||||
// <p className="text-gray-600 dark:text-gray-300 font-poppins text-sm leading-relaxed">
|
||||
// {benefit.description}
|
||||
// </p>
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
// </section>
|
||||
|
||||
// {/* --- 7. Data Outputs & Insights --- */}
|
||||
// <section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
// <div className="container mx-auto px-6">
|
||||
// <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
// <div>
|
||||
// <FaChartPie
|
||||
// className="text-5xl text-gold-500 mb-4"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h2 className="text-3xl md:text-4xl font-bold mb-6 font-poppins text-gray-900 dark:text-white leading-tight">
|
||||
// Actionable Intelligence at Your Fingertips
|
||||
// </h2>
|
||||
// <p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed mb-6 font-poppins">
|
||||
// OBSE delivers a suite of standard financial summaries and
|
||||
// detailed data points essential for informed decisions. Gain
|
||||
// clarity on:
|
||||
// </p>
|
||||
// <div className="space-y-4">
|
||||
// {dataOutputs.map((output) => (
|
||||
// <div
|
||||
// key={output.title}
|
||||
// className="flex items-start p-3 bg-gray-50 dark:bg-gray-800 rounded-md border-l-4 border-gold-500/50"
|
||||
// style={{ borderColor: "rgba(225, 196, 74, 0.5)" }}
|
||||
// >
|
||||
// <output.icon
|
||||
// className="text-2xl text-gold-500 mt-1 mr-4 flex-shrink-0"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <div>
|
||||
// <h4 className="font-semibold font-poppins dark:text-white">
|
||||
// {output.title}
|
||||
// </h4>
|
||||
// <p className="text-sm text-gray-600 dark:text-gray-300 font-poppins">
|
||||
// {output.description}
|
||||
// </p>
|
||||
// </div>
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
// <div className="relative h-80 md:h-[500px] rounded-lg overflow-hidden shadow-lg bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
|
||||
// {/* Visual: Use dashboard examples from Page 6 */}
|
||||
// <Image
|
||||
// src="/images/obse-dashboard-visuals.png" // Placeholder: Catalogue Page 6 Dashboards
|
||||
// alt="OBSE Dashboard Examples showing financial insights"
|
||||
// layout="fill"
|
||||
// objectFit="contain" // Contain might be better for UI elements
|
||||
// className="p-4"
|
||||
// />
|
||||
// <p className="absolute text-gray-500 dark:text-gray-400 z-0">
|
||||
// Dashboard Visual (Replace with Image)
|
||||
// </p>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </section>
|
||||
|
||||
// {/* --- 8. Technology Deep Dive --- */}
|
||||
// <section className="py-16 md:py-24 bg-gray-800 text-white">
|
||||
// <div className="container mx-auto px-6">
|
||||
// <div className="text-center mb-14">
|
||||
// <FaMicrochip
|
||||
// className="text-5xl text-gold-500 mx-auto mb-5"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h2 className="text-3xl md:text-4xl font-bold font-poppins text-white mb-4">
|
||||
// Leveraging Advanced Technology
|
||||
// </h2>
|
||||
// <p className="text-md md:text-lg text-gray-300 max-w-3xl mx-auto font-poppins">
|
||||
// Built on a foundation of robust and intelligent technologies for
|
||||
// reliable, accurate results.
|
||||
// </p>
|
||||
// </div>
|
||||
// <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
// {/* OCR Engine */}
|
||||
// <div className="bg-gray-700 p-6 rounded-lg text-center">
|
||||
// <FaBolt
|
||||
// className="text-4xl text-gold-500 mx-auto mb-3"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h4 className="text-xl font-semibold mb-2 font-poppins text-white">
|
||||
// Advanced OCR Engine
|
||||
// </h4>
|
||||
// <p className="text-gray-300 font-poppins text-sm">
|
||||
// Fine-tuned for SA bank statements, handles diverse formats,
|
||||
// scans, and obscured text.
|
||||
// </p>
|
||||
// </div>
|
||||
// {/* Machine Learning */}
|
||||
// <div className="bg-gray-700 p-6 rounded-lg text-center">
|
||||
// <FaBrain
|
||||
// className="text-4xl text-gold-500 mx-auto mb-3"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h4 className="text-xl font-semibold mb-2 font-poppins text-white">
|
||||
// Machine Learning
|
||||
// </h4>
|
||||
// <p className="text-gray-300 font-poppins text-sm">
|
||||
// Continuously improves interpretation, reconciles discrepancies,
|
||||
// and powers fraud detection models.
|
||||
// </p>
|
||||
// </div>
|
||||
// {/* Data Structuring */}
|
||||
// <div className="bg-gray-700 p-6 rounded-lg text-center">
|
||||
// <FaTable
|
||||
// className="text-4xl text-gold-500 mx-auto mb-3"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h4 className="text-xl font-semibold mb-2 font-poppins text-white">
|
||||
// Data Structuring & Validation
|
||||
// </h4>
|
||||
// <p className="text-gray-300 font-poppins text-sm">
|
||||
// Intelligently structures extracted data (e.g., JSON), validates
|
||||
// entries, applies rules.
|
||||
// </p>
|
||||
// </div>
|
||||
// {/* Secure API */}
|
||||
// <div className="bg-gray-700 p-6 rounded-lg text-center">
|
||||
// <FaNetworkWired
|
||||
// className="text-4xl text-gold-500 mx-auto mb-3"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h4 className="text-xl font-semibold mb-2 font-poppins text-white">
|
||||
// Secure API
|
||||
// </h4>
|
||||
// <p className="text-gray-300 font-poppins text-sm">
|
||||
// Robust and secure API for seamless, safe integration with your
|
||||
// existing systems.
|
||||
// </p>
|
||||
// </div>
|
||||
// </div>
|
||||
// {/* Visual: JSON code snippet visual from Page 5 */}
|
||||
// <div className="mt-12 relative h-48 md:h-64 rounded-lg overflow-hidden shadow-lg bg-black flex items-center justify-center">
|
||||
// <Image
|
||||
// src="/images/obse-json-output.png" // Placeholder: Catalogue Page 5 JSON Visual
|
||||
// alt="Example JSON output from OBSE data extraction"
|
||||
// layout="fill"
|
||||
// objectFit="contain"
|
||||
// className="p-4 opacity-80"
|
||||
// />
|
||||
// <p className="absolute text-gray-500 z-0">
|
||||
// JSON Output Example (Replace with Image)
|
||||
// </p>
|
||||
// </div>
|
||||
// </div>
|
||||
// </section>
|
||||
|
||||
// {/* --- 9. Use Cases & Applications --- */}
|
||||
// <section className="py-16 md:py-24 bg-gray-50 dark:bg-gray-800">
|
||||
// <div className="container mx-auto px-6">
|
||||
// <div className="text-center mb-14">
|
||||
// <FaBriefcase
|
||||
// className="text-5xl text-gold-500 mx-auto mb-5"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
// Empowering Various Financial Processes
|
||||
// </h2>
|
||||
// <p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto font-poppins">
|
||||
// OBSE delivers value across a wide range of applications within the
|
||||
// financial services sector.
|
||||
// </p>
|
||||
// </div>
|
||||
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
// {useCases.map((useCase) => (
|
||||
// <div
|
||||
// key={useCase.title}
|
||||
// className="bg-white dark:bg-gray-700 p-5 rounded-lg shadow-sm border-l-4 border-gold-500 flex items-center"
|
||||
// style={{ borderColor: "#e1c44a" }}
|
||||
// >
|
||||
// <FaCheckCircle
|
||||
// className="text-gold-500 mr-3 flex-shrink-0"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <span className="font-poppins dark:text-white text-sm font-medium">
|
||||
// {useCase.title}
|
||||
// </span>
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
// </section>
|
||||
|
||||
// {/* --- 10. Customization & Partnership --- */}
|
||||
// <section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
// <div className="container mx-auto px-6">
|
||||
// <div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
||||
// <div className="relative h-64 md:h-80 rounded-lg overflow-hidden shadow-lg order-last md:order-first">
|
||||
// <Image
|
||||
// src="/images/partnership-handshake.jpg" // Placeholder: Image representing partnership
|
||||
// alt="OMS and Client Partnership"
|
||||
// layout="fill"
|
||||
// objectFit="cover"
|
||||
// className="transition-transform duration-500 hover:scale-105"
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <FaUsersCog
|
||||
// className="text-5xl text-gold-500 mb-4"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h2 className="text-3xl md:text-4xl font-bold mb-6 font-poppins text-gray-900 dark:text-white leading-tight">
|
||||
// Flexible Solutions, Tailored To You
|
||||
// </h2>
|
||||
// <p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed mb-4 font-poppins">
|
||||
// While OBSE offers a comprehensive standard package, we
|
||||
// understand that each organization has unique needs. If you
|
||||
// require specific information or analysis not included,{" "}
|
||||
// <strong className="dark:text-white">
|
||||
// we are committed to collaborating with you to develop
|
||||
// tailor-made outputs
|
||||
// </strong>{" "}
|
||||
// that align perfectly with your requirements.
|
||||
// </p>
|
||||
// <p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed mb-6 font-poppins">
|
||||
// Our goal is to ensure OBSE meets and exceeds your expectations,
|
||||
// providing the exact insights needed for your operational
|
||||
// processes and decision-making.
|
||||
// </p>
|
||||
// {/* POC Callout */}
|
||||
// <div
|
||||
// className="bg-gold-50 dark:bg-gold-900/20 border-l-4 border-gold-500 p-4 rounded-md"
|
||||
// style={{ borderColor: "#e1c44a" }}
|
||||
// >
|
||||
// <h4 className="font-semibold font-poppins text-gold-800 dark:text-gold-300">
|
||||
// Proof of Concept (POC) Available
|
||||
// </h4>
|
||||
// <p className="text-sm text-gold-700 dark:text-gold-400 font-poppins mt-1">
|
||||
// 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.
|
||||
// </p>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </section>
|
||||
|
||||
// {/* --- 11. About OMS: Your Trusted Technology Partner --- */}
|
||||
// <section className="py-16 md:py-24 bg-gray-100 dark:bg-gray-800">
|
||||
// <div className="container mx-auto px-6 text-center">
|
||||
// {/* Optional: Simple OMS logo here */}
|
||||
// <Image
|
||||
// src="/images/oms-logo-dark.png" // Placeholder: Dark version of logo
|
||||
// alt="Owethu Management Solutions Logo"
|
||||
// width={150}
|
||||
// height={75}
|
||||
// className="h-12 w-auto mx-auto mb-6"
|
||||
// />
|
||||
// <h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
// Your Experienced Technology Partner
|
||||
// </h2>
|
||||
// <p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto font-poppins mb-6">
|
||||
// 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.
|
||||
// </p>
|
||||
// <p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto font-poppins font-semibold">
|
||||
// We focus on leveraging technology to solve real-world business
|
||||
// challenges in the financial sector, emphasizing efficiency,
|
||||
// accuracy, and strong partnerships.
|
||||
// </p>
|
||||
// <div className="mt-8">
|
||||
// <Link
|
||||
// href="/about-us"
|
||||
// className="inline-block text-gold-600 dark:text-gold-400 font-semibold py-2 px-4 rounded-md hover:text-gold-700 dark:hover:text-gold-300 hover:bg-gold-500/10 transition-colors duration-300 font-poppins"
|
||||
// style={{ color: "#c8a93d" }} // Using a slightly darker gold for text link
|
||||
// >
|
||||
// Learn More About OMS <FaArrowRight className="inline ml-1" />
|
||||
// </Link>
|
||||
// </div>
|
||||
// </div>
|
||||
// </section>
|
||||
|
||||
// {/* --- 12. Call to Action (CTA) --- */}
|
||||
// <section
|
||||
// className="py-16 md:py-24 bg-gradient-to-r from-gold-500 to-gold-600 text-gray-900 dark:from-gold-600 dark:to-gold-700"
|
||||
// style={{ backgroundColor: "#e1c44a" }}
|
||||
// >
|
||||
// <div className="container mx-auto px-6 text-center">
|
||||
// <FaBolt className="text-5xl mx-auto mb-5 text-gray-800" />
|
||||
// <h2 className="text-3xl md:text-4xl font-bold mb-4 font-poppins text-gray-900">
|
||||
// Ready to Transform Your Bank Statement Processing?
|
||||
// </h2>
|
||||
// <p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed font-poppins text-gray-800 mb-8">
|
||||
// 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.
|
||||
// </p>
|
||||
// <div className="flex flex-col sm:flex-row justify-center gap-4">
|
||||
// <Link
|
||||
// href="#contact" // Link to contact form/section below
|
||||
// className="inline-block bg-gray-800 text-white font-bold py-3 px-8 rounded-md hover:bg-gray-900 dark:bg-gray-900 dark:hover:bg-black transition-colors duration-300 font-poppins"
|
||||
// >
|
||||
// Request a Personalized Demo
|
||||
// </Link>
|
||||
// <Link
|
||||
// href="/contact" // Link to general contact page
|
||||
// className="inline-block bg-transparent border-2 border-gray-800 text-gray-800 font-bold py-3 px-8 rounded-md hover:bg-gray-800/10 transition-colors duration-300 font-poppins"
|
||||
// >
|
||||
// Contact Sales Team
|
||||
// </Link>
|
||||
// </div>
|
||||
// {/* Optional Tertiary Link */}
|
||||
// {/* <div className="mt-4">
|
||||
// <a href="/path/to/obse-brochure.pdf" target="_blank" rel="noopener noreferrer" className="text-sm font-medium text-gray-700 hover:text-black font-poppins">
|
||||
// Download Brochure
|
||||
// </a>
|
||||
// </div> */}
|
||||
// </div>
|
||||
// </section>
|
||||
|
||||
// {/* --- 13. Contact Information --- */}
|
||||
// <section
|
||||
// id="contact"
|
||||
// className="py-16 md:py-24 bg-white dark:bg-gray-900"
|
||||
// >
|
||||
// <div className="container mx-auto px-6">
|
||||
// <div className="text-center mb-12">
|
||||
// <FaPhoneAlt
|
||||
// className="text-5xl text-gold-500 mx-auto mb-5"
|
||||
// style={{ color: "#e1c44a" }}
|
||||
// />
|
||||
// <h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
// Get in Touch
|
||||
// </h2>
|
||||
// <p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto font-poppins">
|
||||
// For personalized support or to schedule your demo, reach out to
|
||||
// us. Let's harness the power of automated data extraction for your
|
||||
// business.
|
||||
// </p>
|
||||
// </div>
|
||||
// <div className="max-w-xl mx-auto text-center">
|
||||
// <div className="mb-6">
|
||||
// <h4 className="text-lg font-semibold font-poppins dark:text-white">
|
||||
// Contact Details:
|
||||
// </h4>
|
||||
// <p className="text-gray-700 dark:text-gray-300 font-poppins mt-2">
|
||||
// <strong className="dark:text-white">Phone:</strong>{" "}
|
||||
// <a
|
||||
// href="tel:+27120513281"
|
||||
// className="text-gold-600 hover:text-gold-700 dark:text-gold-400 dark:hover:text-gold-300"
|
||||
// >
|
||||
// (012) 051 3281
|
||||
// </a>
|
||||
// </p>
|
||||
// <p className="text-gray-700 dark:text-gray-300 font-poppins mt-1">
|
||||
// <strong className="dark:text-white">Email:</strong>{" "}
|
||||
// <a
|
||||
// href="mailto:Zanelem@oms.africa"
|
||||
// className="text-gold-600 hover:text-gold-700 dark:text-gold-400 dark:hover:text-gold-300"
|
||||
// >
|
||||
// Zanelem@oms.africa
|
||||
// </a>
|
||||
// </p>
|
||||
// </div>
|
||||
|
||||
// {/* Optional: Placeholder for Contact Form */}
|
||||
// {/* <div className="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||
// <h4 className="text-lg font-semibold font-poppins dark:text-white mb-4">Or Send Us a Message</h4>
|
||||
// <p className="text-gray-600 dark:text-gray-400 font-poppins">Contact form component would go here.</p>
|
||||
// <Link href="/contact" className="mt-4 inline-block bg-gold-500 text-gray-900 font-bold py-2 px-6 rounded-md hover:bg-gold-600 transition-colors duration-300 font-poppins" style={{backgroundColor: "#e1c44a"}}>Go to Contact Form</Link>
|
||||
// </div> */}
|
||||
// </div>
|
||||
// </div>
|
||||
// </section>
|
||||
// </>
|
||||
// );
|
||||
// }
|
||||
847
app/(website)/obse/page.tsx
Normal file
@ -0,0 +1,847 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Metadata } from "next"; // Added import
|
||||
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
|
||||
|
||||
// SEO Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: "Automated Bank Statement Extraction South Africa | OBSE by OMS",
|
||||
description:
|
||||
"Revolutionize lending with OBSE: Automated bank statement data extraction, OCR, fraud detection, and analysis for South African financial institutions. Improve accuracy, speed, and decision-making.",
|
||||
keywords: [
|
||||
"Optimised Bank Statement Extractor",
|
||||
"OBSE",
|
||||
"bank statement extraction",
|
||||
"OCR bank statements",
|
||||
"automated financial data",
|
||||
"South Africa",
|
||||
"lending automation",
|
||||
"credit risk assessment",
|
||||
"income verification",
|
||||
"affordability calculation",
|
||||
"fraud detection",
|
||||
"financial technology",
|
||||
"fintech SA",
|
||||
"OMS",
|
||||
"Owethu Management Services",
|
||||
],
|
||||
// Optional: Add Open Graph and Twitter Card metadata for social sharing
|
||||
openGraph: {
|
||||
title: "Automated Bank Statement Extraction South Africa | OBSE by OMS",
|
||||
description:
|
||||
"Streamline your financial processes with OBSE's intelligent bank statement analysis.",
|
||||
url: "https://www.oms.africa/obse", // Replace with your actual URL
|
||||
images: [
|
||||
{
|
||||
url: "/images/obse-og-image.png", // Replace with an appropriate OG image URL
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "OBSE Automated Bank Statement Extraction",
|
||||
},
|
||||
],
|
||||
locale: "en_ZA",
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Automated Bank Statement Extraction South Africa | OBSE by OMS",
|
||||
description:
|
||||
"Fast, accurate bank statement processing for SA lenders with OBSE.",
|
||||
// images: ['/images/obse-twitter-image.png'], // Replace with Twitter-specific image if needed
|
||||
},
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<div className="bg-white text-gray-800 overflow-x-hidden dark:bg-gray-900 dark:text-gray-200 font-poppins">
|
||||
{/* 1. Hero Section */}
|
||||
<section className="relative bg-gradient-to-r from-gray-800 via-gray-700 to-gray-900 text-white py-20 md:py-32">
|
||||
<div className="absolute inset-0 bg-black opacity-40 dark:opacity-60"></div>
|
||||
{/* Optional: Add a subtle background pattern or image */}
|
||||
{/* <div className="absolute inset-0 bg-[url('/path/to/hero-bg.png')] bg-cover bg-center opacity-10"></div> */}
|
||||
<div className="container mx-auto px-6 text-center relative z-10">
|
||||
{/* Consider adding OMS Logo here */}
|
||||
{/* <Image src="/oms-logo-white.png" alt="OMS Logo" width={150} height={50} className="mx-auto mb-6" /> */}
|
||||
<h1
|
||||
className="text-3xl md:text-5xl lg:text-6xl font-bold mb-4 font-poppins drop-shadow-md"
|
||||
style={{ color: COLORS.primary }} // Use gold color
|
||||
>
|
||||
Revolutionize Your Lending and Credit Processes with OBSE
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl max-w-4xl mx-auto leading-relaxed text-gray-200 dark:text-gray-300 mb-8">
|
||||
Automate data extraction, enhance accuracy, detect fraud, and
|
||||
accelerate decision-making with South Africa's intelligent bank
|
||||
statement solution.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row justify-center items-center gap-4">
|
||||
<Link
|
||||
href="#how-it-works" // Link to the "How it Works" section
|
||||
className="inline-flex items-center justify-center bg-gold-500 text-gray-900 font-bold py-3 px-8 rounded-md hover:bg-gold-600 transition-colors duration-300"
|
||||
style={{ backgroundColor: COLORS.primary }}
|
||||
>
|
||||
<FaPlayCircle className="inline mr-2 text-red-400" /> See How It
|
||||
Works
|
||||
</Link>
|
||||
{/* Optional Mini-CTA */}
|
||||
{/* <Link href="#overview-video" className="inline-flex items-center text-gold-400 hover:text-gold-300 transition-colors duration-300">
|
||||
<FaPlayCircle className="mr-2" /> Watch a Quick Overview
|
||||
</Link> */}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 2. The Challenge Section */}
|
||||
<section className="py-16 md:py-24 bg-gray-50 dark:bg-gray-800">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
The High Cost of Manual Bank Statement Processing
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
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) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md text-center transform transition duration-300 hover:-translate-y-1 hover:shadow-lg"
|
||||
>
|
||||
<item.icon
|
||||
className="text-4xl text-red-500 dark:text-red-400 mx-auto mb-4" // Using red to signify pain points
|
||||
/>
|
||||
<h4 className="text-xl font-semibold mb-2 font-poppins dark:text-white">
|
||||
{item.title}
|
||||
</h4>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm">
|
||||
{item.desc}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3. Introducing OBSE Section */}
|
||||
<section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 font-poppins text-gray-900 dark:text-white leading-tight">
|
||||
OBSE: Automated Accuracy, Speed, and Insight
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed mb-4">
|
||||
Optimised 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.
|
||||
</p>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed font-semibold">
|
||||
OBSE transforms raw bank statement data into actionable
|
||||
financial intelligence in seconds, eliminating manual drudgery
|
||||
and empowering your teams to make faster, smarter decisions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative h-64 md:h-80 rounded-lg overflow-hidden shadow-lg">
|
||||
{/* Placeholder for UI Screenshot */}
|
||||
<Image
|
||||
src="/images/obse.svg" // Replace with actual path
|
||||
alt="OBSE Upload Interface"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="bg-gray-200 dark:bg-gray-700" // Placeholder background
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 4. How OBSE Works Section */}
|
||||
<section
|
||||
id="how-it-works"
|
||||
className="py-16 md:py-24 bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-700"
|
||||
>
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
From PDF to Insights in Seconds
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
OBSE simplifies complex data extraction into a streamlined
|
||||
workflow.
|
||||
</p>
|
||||
</div>
|
||||
{/* Consider a visual flow diagram or stepped cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6 text-center">
|
||||
{[
|
||||
{
|
||||
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) => (
|
||||
<div
|
||||
key={item.step}
|
||||
className="bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md"
|
||||
>
|
||||
<div
|
||||
className="w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-4 text-white font-bold text-xl"
|
||||
style={{ backgroundColor: COLORS.primary }}
|
||||
>
|
||||
{item.step}
|
||||
</div>
|
||||
<item.icon
|
||||
className="text-3xl text-gold-500 dark:text-gold-400 mx-auto mb-3"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h4 className="text-lg font-semibold mb-2 font-poppins dark:text-white">
|
||||
{item.title}
|
||||
</h4>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm">
|
||||
{item.desc}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 5. Key Features Section */}
|
||||
<section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Unlock Deeper Financial Understanding
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
Explore the powerful capabilities that drive OBSE's
|
||||
performance.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{keyFeatures.map((feature) => (
|
||||
<div
|
||||
key={feature.title}
|
||||
className="relative bg-gray-50 dark:bg-gray-800 p-6 rounded-lg shadow-sm border-t-4 border-gold-500 dark:border-gold-400 transition-shadow hover:shadow-md"
|
||||
style={{ borderColor: COLORS.primary }}
|
||||
>
|
||||
{feature.isComingSoon && (
|
||||
<span className="absolute top-2 right-2 px-3 py-1 text-sm font-bold bg-cyan-700 text-white rounded-full shadow-lg z-10 font-poppins">
|
||||
Coming Soon
|
||||
</span>
|
||||
)}
|
||||
<feature.icon
|
||||
className="text-3xl mb-3 text-gold-500 dark:text-gold-400"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h4 className="text-lg font-semibold mb-2 font-poppins dark:text-white">
|
||||
{feature.title}
|
||||
</h4>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm leading-relaxed">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 6. Core Benefits Section */}
|
||||
<section className="py-16 md:py-24 bg-gray-50 dark:bg-gray-800">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
The OBSE Advantage: Transform Your Operations
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
Experience tangible benefits that impact your bottom line and
|
||||
efficiency.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{coreBenefits.map((benefit) => (
|
||||
<div
|
||||
key={benefit.title}
|
||||
className="bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md text-center transform transition duration-300 hover:-translate-y-1 hover:shadow-lg"
|
||||
>
|
||||
<benefit.icon
|
||||
className="text-4xl text-gold-500 dark:text-gold-400 mx-auto mb-4"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h4 className="text-lg font-semibold mb-2 font-poppins dark:text-white">
|
||||
{benefit.title}
|
||||
</h4>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm leading-relaxed">
|
||||
{benefit.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 7. Data Outputs & Insights Section */}
|
||||
<section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
||||
<div className="relative h-64 md:h-96 rounded-lg overflow-hidden shadow-lg">
|
||||
{/* Placeholder for Dashboard Example */}
|
||||
<Image
|
||||
src="/images/dashboard.png" // Replace with actual path
|
||||
alt="OBSE Dashboard Insights"
|
||||
layout="fill"
|
||||
objectFit="contain" // Use contain if showing a full dashboard
|
||||
className="bg-gray-100 dark:bg-gray-800 p-4" // Add padding if needed
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 font-poppins text-gray-900 dark:text-white leading-tight">
|
||||
Actionable Intelligence at Your Fingertips
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed mb-6">
|
||||
OBSE delivers a suite of standard financial summaries and
|
||||
detailed data points essential for informed decisions:
|
||||
</p>
|
||||
<ul className="space-y-3">
|
||||
{dataOutputs.map((output) => (
|
||||
<li key={output} className="flex items-start">
|
||||
<FaCheckCircle
|
||||
className="w-5 h-5 text-green-500 dark:text-green-400 mr-3 mt-1 flex-shrink-0"
|
||||
style={{ color: COLORS.primary }} // Or use primary color
|
||||
/>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{output}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 8. Technology Deep Dive Section */}
|
||||
<section className="py-16 md:py-24 bg-gray-800 text-white">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<FaCogs
|
||||
className="text-5xl text-gold-500 mx-auto mb-4"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins mb-4">
|
||||
Leveraging Advanced Technology for Reliable Results
|
||||
</h2>
|
||||
<p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed text-gray-300">
|
||||
Built on a foundation of cutting-edge technology to ensure
|
||||
performance and accuracy.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-6xl mx-auto">
|
||||
{[
|
||||
{
|
||||
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) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="bg-gray-700 p-6 rounded-lg text-center"
|
||||
>
|
||||
<item.icon
|
||||
className="text-4xl text-gold-500 mx-auto mb-4"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h4 className="text-xl font-semibold mb-2 font-poppins text-white">
|
||||
{item.title}
|
||||
</h4>
|
||||
<p className="text-gray-300 text-sm">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Optional: Add JSON snippet visual */}
|
||||
{/* <div className="mt-12 max-w-2xl mx-auto bg-gray-900 p-4 rounded-lg shadow-lg">
|
||||
<pre><code className="language-json text-sm">
|
||||
{`{
|
||||
"transaction_id": "T12345",
|
||||
"date": "2023-10-26",
|
||||
"description": "SALARY ABC CORP",
|
||||
"amount": 15000.00,
|
||||
"type": "credit",
|
||||
"category": "Income/Salary"
|
||||
}`}
|
||||
</code></pre>
|
||||
</div> */}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 9. Use Cases & Applications Section */}
|
||||
<section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<FaWrench
|
||||
className="text-5xl text-gold-500 mx-auto mb-4"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Empowering Various Financial Processes
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
OBSE provides critical data and automation for a wide range of
|
||||
applications.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-6 max-w-5xl mx-auto">
|
||||
{useCases.map((useCase) => (
|
||||
<div
|
||||
key={useCase}
|
||||
className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg shadow-sm text-center"
|
||||
>
|
||||
<p className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
{useCase}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 10. Customization & Partnership Section */}
|
||||
<section className="py-16 md:py-24 bg-gray-50 dark:bg-gray-800">
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
<FaHandshake
|
||||
className="text-5xl text-gold-500 mx-auto mb-4"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Flexible Solutions Tailored to Your Needs
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-4xl mx-auto mb-6 leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
<p className="text-md md:text-lg text-gray-800 dark:text-gray-200 font-semibold max-w-4xl mx-auto">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 11. About OMS Section (Brief) */}
|
||||
<section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
{/* Optional: OMS Logo */}
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Your Experienced Partner in Financial Technology
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto mb-6">
|
||||
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.
|
||||
</p>
|
||||
<Link
|
||||
href="/about"
|
||||
className="text-gold-600 dark:text-gold-400 hover:text-gold-700 dark:hover:text-gold-300 font-semibold transition-colors duration-300 inline-flex items-center"
|
||||
style={{ color: COLORS.primary }}
|
||||
>
|
||||
Learn More About OMS <FaArrowRight className="ml-2" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 12. Call to Action (CTA) Section */}
|
||||
<section
|
||||
className="py-16 md:py-24 bg-gold-500 text-gray-900"
|
||||
style={{ backgroundColor: COLORS.primary }}
|
||||
>
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4 font-poppins">
|
||||
Ready to Transform Your Bank Statement Processing?
|
||||
</h2>
|
||||
<p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed mb-8 font-poppins text-gray-800">
|
||||
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.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row justify-center items-center gap-4">
|
||||
<Link
|
||||
href="/contact?subject=OBSE Demo Request" // Pre-fill subject for contact form
|
||||
className="inline-block bg-gray-800 text-white font-bold py-3 px-8 rounded-md hover:bg-gray-900 dark:bg-gray-900 dark:hover:bg-black transition-colors duration-300"
|
||||
>
|
||||
Request a Personalized Demo
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact?subject=OBSE Sales Inquiry" // Pre-fill subject
|
||||
className="inline-block bg-transparent border-2 border-gray-800 text-gray-800 font-bold py-3 px-8 rounded-md hover:bg-gray-800 hover:text-white transition-colors duration-300"
|
||||
>
|
||||
Contact Sales Team
|
||||
</Link>
|
||||
</div>
|
||||
{/* Optional: Download Brochure Link */}
|
||||
{/* <div className="mt-6">
|
||||
<a href="/path/to/obse-brochure.pdf" download className="text-sm text-gray-800 hover:underline">
|
||||
Download Brochure
|
||||
</a>
|
||||
</div> */}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 13. Contact Information Section */}
|
||||
<section className="py-16 md:py-24 bg-gray-100 dark:bg-gray-800">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Get in Touch
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
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!
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
|
||||
{/* Contact Details */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<FaPhone
|
||||
className="w-6 h-6 text-gold-500 dark:text-gold-400 flex-shrink-0"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold font-poppins text-gray-800 dark:text-gray-100">
|
||||
Phone
|
||||
</h3>
|
||||
<a
|
||||
href="tel:+27120513281" // Updated phone number
|
||||
className="text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100 transition text-sm"
|
||||
>
|
||||
(012) 051 3281
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<FaEnvelope
|
||||
className="w-6 h-6 text-gold-500 dark:text-gold-400 flex-shrink-0"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold font-poppins text-gray-800 dark:text-gray-100">
|
||||
Email
|
||||
</h3>
|
||||
<a
|
||||
href="mailto:Zanelem@oms.africa" // Updated email
|
||||
className="text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100 transition text-sm"
|
||||
>
|
||||
Zanelem@oms.africa
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/* Optional: Link to main contact page */}
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-flex items-center text-gold-600 dark:text-gold-400 hover:text-gold-700 dark:hover:text-gold-300 font-semibold transition-colors duration-300"
|
||||
style={{ color: COLORS.primary }}
|
||||
>
|
||||
Go to Full Contact Page <FaArrowRight className="ml-2" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Optional: Simple Contact Form (if ContactForm component is not used/available) */}
|
||||
{/* <form className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Name</label>
|
||||
<input type="text" id="name" name="name" required className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-gold-500 focus:border-gold-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
|
||||
<input type="email" id="email" name="email" required className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-gold-500 focus:border-gold-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Message</label>
|
||||
<textarea id="message" name="message" rows={4} required className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-gold-500 focus:border-gold-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"></textarea>
|
||||
</div>
|
||||
<button type="submit" className="w-full bg-gold-500 text-gray-900 font-bold py-2 px-4 rounded-md hover:bg-gold-600 transition-colors duration-300" style={{ backgroundColor: COLORS.primary }}>
|
||||
Send Message
|
||||
</button>
|
||||
</form> */}
|
||||
|
||||
{/* OR Use the ContactForm Component */}
|
||||
<div className="bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md">
|
||||
<h3 className="text-xl font-semibold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Send a Quick Inquiry
|
||||
</h3>
|
||||
<ContactForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -12,48 +12,37 @@ import WhyChooseUsSection, {
|
||||
import FeaturedProductSection, {
|
||||
defaultObseFeatures,
|
||||
} from "./_components/FeaturedProductSection";
|
||||
// import HeroSectionModern from "./_components/HeroSectionModern";
|
||||
// import HeroSectionDynamic from "./_components/HeroSectionDynamic";
|
||||
import { getHome } from "@/lib/query/home";
|
||||
|
||||
export default async function HomePage() {
|
||||
// Explicitly type the data variable, assuming getHome returns HeroSectionType or null/undefined
|
||||
const data = await getHome();
|
||||
|
||||
// Handle case where data might be null or undefined
|
||||
if (!data) {
|
||||
// Optionally return a loading state or default content
|
||||
return <div>Loading hero section...</div>;
|
||||
// Or render the HeroSection with default props if preferred
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
{/*
|
||||
<HeroSectionDynamic
|
||||
title={<>Where Innovation Meets Excellence</>} // Simplified title for this layout
|
||||
subtitle="We deliver cutting-edge IT solutions, empowering businesses to thrive in the ever-evolving digital landscape with unmatched industry expertise."
|
||||
buttonText="Explore Solutions" // Changed button text slightly
|
||||
buttonHref="/services" // Point to services maybe?
|
||||
imageUrl="/hero-bg.jpg" // Use a different, high-quality background image
|
||||
/>
|
||||
|
||||
|
||||
|
||||
<HeroSectionModern
|
||||
title={
|
||||
<>
|
||||
Where Innovation <br className="hidden md:block" /> Meets
|
||||
Excellence.
|
||||
</>
|
||||
}
|
||||
subtitle="Welcome to Owethu Managed Services. We deliver cutting-edge IT solutions, empowering businesses to thrive in the ever-evolving digital landscape with unmatched industry expertise."
|
||||
buttonText="Learn More"
|
||||
buttonHref="/about"
|
||||
imageUrl="/hero-bg.jpg" // Specify your hero image
|
||||
/>
|
||||
*/}
|
||||
<HeroSection
|
||||
title={
|
||||
<>
|
||||
Where Innovation <br className="hidden md:block" /> Meets
|
||||
Excellence.
|
||||
{data?.hero_title} <br className="hidden md:block" />
|
||||
</>
|
||||
}
|
||||
subtitle="Welcome to Owethu Managed Services. We deliver cutting-edge IT solutions, empowering businesses to thrive in the ever-evolving digital landscape with unmatched industry expertise."
|
||||
buttonText="Learn More"
|
||||
buttonHref="/about"
|
||||
imageUrl="/hero-bg.jpg" // Specify your hero image
|
||||
subtitle={
|
||||
data.hero_subtitle || "Your trusted partner in technology solutions."
|
||||
} // Use optional chaining and provide a default
|
||||
buttonText={data.hero_buttons?.[0]?.label || "Learn More"} // Use optional chaining and provide a default
|
||||
buttonHref={data.hero_buttons?.[0]?.link || "/about"} // Use optional chaining and provide a default
|
||||
imageUrl={
|
||||
data.hero_cover
|
||||
? `${process.env.DIRECTUS_API_ENDPOINT}/assets/${data.hero_cover}`
|
||||
: "/hero-bg.jpg"
|
||||
} // Use optional chaining and provide a default
|
||||
/>
|
||||
<CoreServicesSection
|
||||
title="Core Services"
|
||||
@ -69,11 +58,11 @@ export default function HomePage() {
|
||||
eyebrow="Featured Product"
|
||||
title="Streamline Financial Analysis with"
|
||||
productName="OBSE"
|
||||
description="Our advanced Optical Bank Statement Extractor automates data aggregation, reduces errors, and provides deep financial insights."
|
||||
description="Our advanced Optimised Bank Statement Extractor automates data aggregation, reduces errors, and provides deep financial insights."
|
||||
features={defaultObseFeatures}
|
||||
buttonText="Learn More & Demo"
|
||||
buttonHref="/products/obse" // Link to the OBSE product page
|
||||
imageUrl="/obse-mockup.png" // **IMPORTANT: Create or find a relevant image**
|
||||
imageUrl="/images/obse.svg" // **IMPORTANT: Create or find a relevant image**
|
||||
imageAlt="OBSE Product Interface Mockup"
|
||||
/>
|
||||
<ClientLogosSection
|
||||
|
||||
36
app/(website)/portal/page.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Metadata } from "next";
|
||||
import { FaTools } from "react-icons/fa"; // Using a construction/tools icon
|
||||
import { COLORS } from "@/constants";
|
||||
|
||||
// SEO Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: "Recruitment Portal | OMS South Africa",
|
||||
description:
|
||||
"Our recruitment portal is currently under development. Stay tuned for updates on career opportunities at Owethu Managed Services.",
|
||||
robots: "noindex, nofollow", // Prevent indexing of the coming soon page
|
||||
};
|
||||
|
||||
export default function RecruitmentPortalPage() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-200px)] bg-gray-50 dark:bg-gray-800 text-center px-4 py-16 font-poppins">
|
||||
<FaTools
|
||||
className="text-6xl mb-6"
|
||||
style={{ color: COLORS.primary }} // Use primary color
|
||||
/>
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Recruitment Portal Coming Soon!
|
||||
</h1>
|
||||
<p className="text-lg text-gray-700 dark:text-gray-300 max-w-xl mx-auto mb-8">
|
||||
We are currently building our dedicated recruitment portal to connect
|
||||
talented individuals with exciting career opportunities at OMS.
|
||||
</p>
|
||||
<p className="text-md text-gray-600 dark:text-gray-400">
|
||||
Please check back later for updates.
|
||||
</p>
|
||||
{/* Optional: Link back to home or contact */}
|
||||
{/* <Link href="/" className="mt-8 inline-block bg-gold-500 text-gray-900 font-bold py-2 px-6 rounded-md hover:bg-gold-600 transition-colors duration-300" style={{ backgroundColor: COLORS.primary }}>
|
||||
Go to Homepage
|
||||
</Link> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
284
app/(website)/services/page.tsx
Normal file
@ -0,0 +1,284 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Metadata } from "next";
|
||||
import {
|
||||
FaArrowRight,
|
||||
FaBriefcase,
|
||||
FaCogs,
|
||||
FaHandshake,
|
||||
FaLaptopCode,
|
||||
FaProjectDiagram,
|
||||
FaUsers,
|
||||
FaWrench,
|
||||
FaShippingFast,
|
||||
} from "react-icons/fa";
|
||||
import { COLORS } from "@/constants"; // Assuming COLORS constant is available
|
||||
|
||||
// SEO Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: "IT Services & Solutions | OMS South Africa",
|
||||
description:
|
||||
"Explore IT services by Owethu Managed Services (OMS): Custom Software Development, IT Resource Augmentation, and the OBSE Bank Statement Extractor. Partner with us for innovative solutions.",
|
||||
keywords: [
|
||||
"IT services South Africa",
|
||||
"custom software development",
|
||||
"IT resource augmentation",
|
||||
"managed IT services",
|
||||
"OBSE",
|
||||
"bank statement automation",
|
||||
"fintech solutions",
|
||||
"IT consulting",
|
||||
"OMS",
|
||||
"Owethu Managed Services",
|
||||
"Centurion",
|
||||
"Gauteng",
|
||||
],
|
||||
openGraph: {
|
||||
title: "Comprehensive IT Services & Solutions | OMS South Africa",
|
||||
description:
|
||||
"OMS offers tailored IT solutions including custom development, resource augmentation, and specialized products like OBSE.",
|
||||
url: "https://oms.africa/services", // Update with the final URL
|
||||
images: [
|
||||
{
|
||||
url: "/images/oms-services-og.png", // Replace with an appropriate OG image URL
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "OMS IT Services",
|
||||
},
|
||||
],
|
||||
locale: "en_ZA",
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Comprehensive IT Services & Solutions | OMS South Africa",
|
||||
description:
|
||||
"Partner with OMS for expert IT services, from custom builds to resource scaling.",
|
||||
// images: ['/images/oms-services-twitter.png'], // Replace if needed
|
||||
},
|
||||
};
|
||||
|
||||
// Data for Service Areas
|
||||
const serviceAreas = [
|
||||
{
|
||||
title: "Custom Software Development & Consulting",
|
||||
icon: FaLaptopCode,
|
||||
description:
|
||||
"We design, build, and implement bespoke software solutions tailored to your unique business challenges and objectives. Our consulting services help align technology with your strategic goals.",
|
||||
imageUrl: "/images/team.svg", // Reusing image from About
|
||||
link: "/contact?subject=Custom Development Inquiry", // Link to contact or a future dedicated page
|
||||
linkText: "Discuss Your Project",
|
||||
},
|
||||
{
|
||||
title: "IT Resource Augmentation",
|
||||
icon: FaUsers,
|
||||
description:
|
||||
"Scale your team effectively with our flexible resource augmentation model. Access skilled IT professionals (BAs, Testers, Developers, Designers) on demand or via managed milestone-based projects.",
|
||||
imageUrl: "/images/team-collaborative.png", // Reusing image from Resource Augmentation
|
||||
link: "/services/resource-augmentation",
|
||||
linkText: "Explore Augmentation",
|
||||
},
|
||||
{
|
||||
title: "OBSE - Bank Statement Extractor",
|
||||
icon: FaShippingFast,
|
||||
description:
|
||||
"Automate bank statement processing with OBSE. Our intelligent OCR solution extracts data accurately and quickly, enhancing efficiency, reducing risk, and speeding up financial assessments.",
|
||||
imageUrl: "/images/obse.svg", // Reusing image from OBSE
|
||||
link: "/obse",
|
||||
linkText: "Learn About OBSE",
|
||||
},
|
||||
];
|
||||
|
||||
const ourApproachItems = [
|
||||
{
|
||||
icon: FaProjectDiagram,
|
||||
title: "Strategic Alignment",
|
||||
description: "Understanding your goals to ensure solutions deliver value.",
|
||||
},
|
||||
{
|
||||
icon: FaHandshake,
|
||||
title: "Client-Centric Collaboration",
|
||||
description: "Working as true partners, involving you throughout.",
|
||||
},
|
||||
{
|
||||
icon: FaCogs,
|
||||
title: "Agile & Adaptive Delivery",
|
||||
description: "Flexible methodologies for faster, relevant results.",
|
||||
},
|
||||
{
|
||||
icon: FaWrench,
|
||||
title: "Technical Excellence",
|
||||
description: "Building robust, scalable, and maintainable solutions.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function ServicesPage() {
|
||||
return (
|
||||
<div className="bg-white text-gray-800 overflow-x-hidden dark:bg-gray-900 dark:text-gray-200 font-poppins">
|
||||
{/* 1. Hero Section */}
|
||||
<section className="relative bg-gradient-to-r from-gray-800 via-gray-700 to-gray-900 text-white py-20 md:py-32">
|
||||
<div className="absolute inset-0 bg-black opacity-50 dark:opacity-70"></div>
|
||||
<div className="container mx-auto px-6 text-center relative z-10">
|
||||
<h1
|
||||
className="text-3xl md:text-5xl lg:text-6xl font-bold mb-4 font-poppins drop-shadow-md"
|
||||
style={{ color: COLORS.primary }}
|
||||
>
|
||||
Tailored IT Services & Solutions
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl max-w-4xl mx-auto leading-relaxed text-gray-200 dark:text-gray-300 mb-8">
|
||||
Owethu Managed Services empowers businesses through innovative
|
||||
technology solutions, strategic consulting, flexible resource
|
||||
augmentation, and specialized product offerings.
|
||||
</p>
|
||||
<Link
|
||||
href="#core-services"
|
||||
className="inline-flex items-center justify-center bg-gold-500 text-gray-900 font-bold py-3 px-8 rounded-md hover:bg-gold-600 transition-colors duration-300"
|
||||
style={{ backgroundColor: COLORS.primary }}
|
||||
>
|
||||
Explore Our Services <FaArrowRight className="ml-2" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 2. Core Service Areas Section */}
|
||||
<section
|
||||
id="core-services"
|
||||
className="py-16 md:py-24 bg-white dark:bg-gray-900"
|
||||
>
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Our Expertise, Your Advantage
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
We offer a comprehensive suite of services designed to address
|
||||
your specific IT needs and drive business growth.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{serviceAreas.map((service) => (
|
||||
<div
|
||||
key={service.title}
|
||||
className="flex flex-col bg-gray-50 dark:bg-gray-800 rounded-lg shadow-md overflow-hidden transform transition duration-300 hover:shadow-xl hover:-translate-y-1"
|
||||
>
|
||||
<div className="relative h-48 w-full">
|
||||
<Image
|
||||
src={service.imageUrl}
|
||||
alt={`${service.title} illustration`}
|
||||
layout="fill"
|
||||
objectFit="contain" // Changed to contain for SVG/Illustrations
|
||||
className="p-4 bg-gray-100 dark:bg-gray-700" // Added padding and bg
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6 flex flex-col flex-grow">
|
||||
<div className="flex items-center mb-3">
|
||||
<service.icon
|
||||
className="text-2xl mr-3 flex-shrink-0"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h3 className="text-xl font-semibold font-poppins text-gray-900 dark:text-white">
|
||||
{service.title}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm mb-4 flex-grow">
|
||||
{service.description}
|
||||
</p>
|
||||
<Link
|
||||
href={service.link}
|
||||
className="inline-flex items-center mt-auto text-gold-600 dark:text-gold-400 hover:text-gold-700 dark:hover:text-gold-300 font-semibold transition-colors duration-300 text-sm"
|
||||
style={{ color: COLORS.primary }}
|
||||
>
|
||||
{service.linkText} <FaArrowRight className="ml-2" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3. Our Approach Section */}
|
||||
<section className="py-16 md:py-24 bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-700">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-14">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
How We Deliver Excellence
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto font-poppins">
|
||||
Our methodology ensures collaboration, precision, and impactful
|
||||
results tailored to your project needs.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{ourApproachItems.map((item) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md text-center transform transition duration-300 hover:-translate-y-2 hover:shadow-xl dark:hover:shadow-gold-500/20"
|
||||
>
|
||||
<item.icon
|
||||
className="text-4xl text-gold-500 mx-auto mb-5"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h4 className="text-xl font-semibold mb-3 font-poppins dark:text-white">
|
||||
{item.title}
|
||||
</h4>
|
||||
<p className="text-gray-600 dark:text-gray-300 font-poppins text-sm leading-relaxed">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 4. Industry Expertise Snippet (Optional) */}
|
||||
<section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
<FaBriefcase
|
||||
className="text-5xl text-gold-500 mx-auto mb-4"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Serving Diverse Industries
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto mb-8">
|
||||
We apply our technical expertise across various sectors,
|
||||
understanding the unique challenges and opportunities within each.
|
||||
Key areas include Financial Services, Automotive, Technology, and
|
||||
more.
|
||||
</p>
|
||||
<Link
|
||||
href="/about#industry-expertise" // Assuming you add an ID to the section in about page
|
||||
className="text-gold-600 dark:text-gold-400 hover:text-gold-700 dark:hover:text-gold-300 font-semibold transition-colors duration-300 inline-flex items-center"
|
||||
style={{ color: COLORS.primary }}
|
||||
>
|
||||
Learn More About Our Industry Focus{" "}
|
||||
<FaArrowRight className="ml-2" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 5. Call to Action (CTA) Section */}
|
||||
<section
|
||||
className="py-16 md:py-24 bg-gold-500 text-gray-900"
|
||||
style={{ backgroundColor: COLORS.primary }}
|
||||
>
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4 font-poppins">
|
||||
Let's Build Your Solution
|
||||
</h2>
|
||||
<p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed mb-8 font-poppins text-gray-800">
|
||||
Ready to discuss how OMS can help you achieve your technology goals?
|
||||
Contact us today for a consultation.
|
||||
</p>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-block bg-gray-800 text-white font-bold py-3 px-8 rounded-md hover:bg-gray-900 dark:bg-gray-900 dark:hover:bg-black transition-colors duration-300"
|
||||
>
|
||||
Get in Touch
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
930
app/(website)/services/resource-augmentation/page.tsx
Normal file
@ -0,0 +1,930 @@
|
||||
// /app/resource-augmentation/page.tsx (or wherever you place your page components)
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Metadata } from "next";
|
||||
import {
|
||||
FaArrowRight,
|
||||
FaBriefcase,
|
||||
FaCheckCircle,
|
||||
FaClipboardList, // For BA
|
||||
FaClock, // For Time & Material
|
||||
FaCode, // For Development (General)
|
||||
FaUsersCog, // For Managed Services/Teams
|
||||
FaPaintBrush, // For UX/UI Design
|
||||
FaProjectDiagram, // For Architecture/Milestones
|
||||
FaPuzzlePiece, // For Integration/Solutions
|
||||
FaRegHandshake, // For Partnership/Client Relations
|
||||
FaShieldAlt, // For Reliability/QA
|
||||
FaSitemap, // For Process/Architecture
|
||||
FaSyncAlt, // For Agile/Flexibility
|
||||
FaTasks, // For Project Management/Milestones
|
||||
FaVial, // For Testing
|
||||
FaUsers, // For Teams/Resources
|
||||
FaChartLine, // For Reporting/MI
|
||||
FaLightbulb, // For Innovation/Solutions
|
||||
FaPhone, // Contact
|
||||
FaEnvelope, // Contact
|
||||
FaTools, // General Capabilities
|
||||
FaLayerGroup, // Frameworks (SAFe, etc.)
|
||||
FaFileInvoiceDollar, // Cost/Pricing
|
||||
FaBusinessTime, // Experience/Past Projects
|
||||
} from "react-icons/fa";
|
||||
import { COLORS } from "@/constants"; // Assuming COLORS constant is available
|
||||
import ContactForm from "@/components/ContactForm"; // Assuming ContactForm is available
|
||||
|
||||
// SEO Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: "IT Resource Augmentation & Managed Services | OMS South Africa",
|
||||
description:
|
||||
"Flexible IT resource augmentation by Owethu Managed Services (OMS). Access expert Business Analysts, Developers, Testers, UX/UI Designers, and more on-demand or via milestone-based projects. Centurion, Gauteng.",
|
||||
keywords: [
|
||||
"IT resource augmentation",
|
||||
"managed IT services",
|
||||
"IT staffing South Africa",
|
||||
"project resources",
|
||||
"IT project delivery",
|
||||
"Business Analyst",
|
||||
"Software Developer",
|
||||
"Test Analyst",
|
||||
"UX/UI Designer",
|
||||
"Process Engineer",
|
||||
"Salesforce Partner",
|
||||
"time and material IT",
|
||||
"milestone-based projects",
|
||||
"IT outsourcing",
|
||||
"flexible IT resources",
|
||||
"Centurion",
|
||||
"Gauteng",
|
||||
"OMS",
|
||||
"Owethu Managed Services",
|
||||
"black female owned IT",
|
||||
],
|
||||
openGraph: {
|
||||
title: "Expert IT Resource Augmentation Services | OMS South Africa",
|
||||
description:
|
||||
"Scale your IT projects effectively with OMS's flexible resource augmentation and managed services.",
|
||||
url: "https://oms.cvevolve.com/resource-augmentation", // Update with the final URL
|
||||
images: [
|
||||
{
|
||||
// Replace with an appropriate OG image URL if you have one
|
||||
url: "/images/oms-resource-augmentation-og.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "OMS IT Resource Augmentation Services",
|
||||
},
|
||||
],
|
||||
locale: "en_ZA",
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Expert IT Resource Augmentation Services | OMS South Africa",
|
||||
description:
|
||||
"Get skilled IT professionals on demand. OMS offers flexible resource augmentation in South Africa.",
|
||||
// images: ['/images/oms-resource-augmentation-twitter.png'], // Replace if needed
|
||||
},
|
||||
};
|
||||
|
||||
// --- Data Structures from PDF ---
|
||||
|
||||
interface ServiceModel {
|
||||
icon: React.ElementType;
|
||||
title: string;
|
||||
description: string;
|
||||
details: string[];
|
||||
pricingModel: string;
|
||||
}
|
||||
|
||||
const serviceModels: ServiceModel[] = [
|
||||
{
|
||||
icon: FaClock,
|
||||
title: "Resource Augmentation Model (Time & Material)",
|
||||
description:
|
||||
"Access skilled IT professionals on demand to supplement your existing team.",
|
||||
details: [
|
||||
"Provides required Technology and Human Resources.",
|
||||
"Client manages the project end-to-end.",
|
||||
"Ideal for adding specific skills or capacity quickly.",
|
||||
"Resources-on-Demand ensures no long-term FTE costs after project completion.",
|
||||
],
|
||||
pricingModel: "Time-and-Material pricing structure.",
|
||||
},
|
||||
{
|
||||
icon: FaTasks,
|
||||
title: "Milestone-Based Model (Managed Service)",
|
||||
description:
|
||||
"OMS takes full responsibility for delivering specific IT project phases or entire projects.",
|
||||
details: [
|
||||
"Includes Project Planning & Management.",
|
||||
"Covers End-to-end Process Design (Requirements, BPD, Arch, UX/UI).",
|
||||
"Includes Data & MI Design (Architecture, Transformation, BI).",
|
||||
"Comprehensive Testing & QA (Strategy, Implementation, Management).",
|
||||
"OMS manages all deployed team members, deliverables, and velocity.",
|
||||
],
|
||||
pricingModel:
|
||||
"Milestone-Based pricing, payable on delivery of key milestones.",
|
||||
},
|
||||
];
|
||||
|
||||
interface Capability {
|
||||
role: string;
|
||||
icon: React.ElementType;
|
||||
description: string;
|
||||
skills: {
|
||||
level: string; // e.g., 'Senior', 'Intermediate', 'Junior'
|
||||
points: string[];
|
||||
}[]; // Simplified for web display
|
||||
}
|
||||
|
||||
// Simplified representation of Page 4 for web display
|
||||
const capabilities: Capability[] = [
|
||||
{
|
||||
role: "Business / System Analysts",
|
||||
icon: FaClipboardList,
|
||||
description:
|
||||
"Bridge the gap between business needs and technology solutions, defining requirements and processes.",
|
||||
skills: [
|
||||
{
|
||||
level: "Senior",
|
||||
points: [
|
||||
"Stakeholder engagement & management",
|
||||
"Lead other BAs",
|
||||
"Requirements/Backlog management",
|
||||
"Process Design",
|
||||
"Report in Steering committees",
|
||||
],
|
||||
},
|
||||
{
|
||||
level: "Intermediate",
|
||||
points: [
|
||||
"Document requirements",
|
||||
"Design solutions with architects",
|
||||
"Stakeholder engagement",
|
||||
],
|
||||
},
|
||||
{
|
||||
level: "Junior",
|
||||
points: ["Elicit requirements", "Document requirements", "UML Design"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "Test Analysts (Manual & Automation)",
|
||||
icon: FaVial,
|
||||
description:
|
||||
"Ensure software quality and reliability through rigorous testing strategies and execution.",
|
||||
skills: [
|
||||
{
|
||||
level: "Senior",
|
||||
points: [
|
||||
"Lead Test Analysts",
|
||||
"Develop Test Strategy",
|
||||
"Manage Test Execution",
|
||||
"UAT Training",
|
||||
"Stakeholder Management",
|
||||
"Report in Steering committees",
|
||||
],
|
||||
},
|
||||
{
|
||||
level: "Intermediate",
|
||||
points: [
|
||||
"Document Test Scenarios",
|
||||
"Write test scripts",
|
||||
"Manage Test Execution",
|
||||
"Report progress",
|
||||
"Execute manual/automated testing",
|
||||
],
|
||||
},
|
||||
{
|
||||
level: "Junior",
|
||||
points: [
|
||||
"Document Test Scenarios",
|
||||
"Execute manual/automated testing",
|
||||
"Produce Test Data",
|
||||
"Defects tracking",
|
||||
"UAT Training",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "UX / UI / CX Designers",
|
||||
icon: FaPaintBrush,
|
||||
description:
|
||||
"Craft intuitive, engaging, and effective user experiences and interfaces.",
|
||||
skills: [
|
||||
{
|
||||
level: "Senior",
|
||||
points: [
|
||||
"Develop Design Led Strategy",
|
||||
"Lead other designers",
|
||||
"Define User Journeys",
|
||||
"Perform user experience testing",
|
||||
"Stakeholder engagement",
|
||||
"Produce UI prototypes",
|
||||
],
|
||||
},
|
||||
{
|
||||
level: "Intermediate",
|
||||
points: [
|
||||
"Define User Journeys",
|
||||
"Perform user experience testing",
|
||||
"Produce design progress reports",
|
||||
"Execute designs for UI prototypes",
|
||||
],
|
||||
},
|
||||
{
|
||||
level: "Junior",
|
||||
points: [
|
||||
"Define User Journeys",
|
||||
"Produce design progress reports",
|
||||
"Execute designs for UI prototypes",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "Process Engineers / Designers",
|
||||
icon: FaSitemap,
|
||||
description:
|
||||
"Analyze, design, and optimize business processes for efficiency and effectiveness.",
|
||||
skills: [
|
||||
{
|
||||
level: "Senior",
|
||||
points: [
|
||||
"Develop Process Design Strategy",
|
||||
"Manage and lead teams",
|
||||
"Define Target Operating Model",
|
||||
"Design ASIS/TOBE Processes",
|
||||
"Stakeholder management",
|
||||
],
|
||||
},
|
||||
{
|
||||
level: "Intermediate",
|
||||
points: [
|
||||
"Design ASIS/TOBE Processes",
|
||||
"Stakeholder management",
|
||||
"Evaluate & remove inefficiencies",
|
||||
"Optimize current processes",
|
||||
],
|
||||
},
|
||||
{
|
||||
level: "Junior",
|
||||
points: [
|
||||
"Design ASIS/TOBE Processes",
|
||||
"Validate & remove inefficiencies",
|
||||
"Optimize current processes",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Note: Developer roles are mentioned in 'About Us' but not detailed in the table. Add a general entry if desired.
|
||||
{
|
||||
role: "Software Developers & Architects",
|
||||
icon: FaCode, // Or FaProjectDiagram for Architects
|
||||
description:
|
||||
"Build, implement, and architect robust and scalable software solutions.",
|
||||
skills: [
|
||||
{
|
||||
level: "General",
|
||||
points: [
|
||||
"Expertise across various modern technologies (backend, frontend, integration)",
|
||||
"Solution architecture design",
|
||||
"Development lifecycle management",
|
||||
"API development and integration",
|
||||
"Database design and management",
|
||||
],
|
||||
},
|
||||
], // Keep generic as PDF doesn't detail levels here
|
||||
},
|
||||
];
|
||||
|
||||
interface Benefit {
|
||||
icon: React.ElementType;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const augmentationBenefits: Benefit[] = [
|
||||
{
|
||||
icon: FaSyncAlt,
|
||||
title: "Ultimate Flexibility",
|
||||
description:
|
||||
"Scale your team up or down quickly based on project demands and budget.",
|
||||
},
|
||||
{
|
||||
icon: FaUsers,
|
||||
title: "Access to Expertise",
|
||||
description:
|
||||
"Gain immediate access to specialized IT skills that may not be available in-house.",
|
||||
},
|
||||
{
|
||||
icon: FaFileInvoiceDollar,
|
||||
title: "Cost-Effectiveness",
|
||||
description:
|
||||
"Reduce recruitment costs, overheads, and long-term commitments associated with FTEs.",
|
||||
},
|
||||
{
|
||||
icon: FaChartLine,
|
||||
title: "Increased Productivity",
|
||||
description:
|
||||
"Focus your core team on strategic initiatives while OMS resources handle specific tasks or projects.",
|
||||
},
|
||||
{
|
||||
icon: FaShieldAlt,
|
||||
title: "Reduced Hiring Burden",
|
||||
description:
|
||||
"Avoid the time-consuming process of sourcing, vetting, hiring, and onboarding new employees.",
|
||||
},
|
||||
{
|
||||
icon: FaTasks,
|
||||
title: "Focus on Core Business",
|
||||
description:
|
||||
"Outsource IT project execution or specific roles, allowing you to concentrate on your primary objectives.",
|
||||
},
|
||||
{
|
||||
icon: FaCheckCircle,
|
||||
title: "Quality Assurance",
|
||||
description:
|
||||
"Benefit from experienced professionals and managed delivery (in Milestone model) for high-quality outcomes.",
|
||||
},
|
||||
{
|
||||
icon: FaLightbulb,
|
||||
title: "Faster Project Delivery",
|
||||
description:
|
||||
"Accelerate project timelines by quickly filling skill gaps and adding capacity.",
|
||||
},
|
||||
];
|
||||
|
||||
const methodologies = [
|
||||
{ name: "Agile (SCRUM)", icon: FaSyncAlt },
|
||||
{ name: "Scaled Agile (SAFe)", icon: FaLayerGroup },
|
||||
{ name: "Waterfall", icon: FaProjectDiagram }, // Using ProjectDiagram as a proxy
|
||||
];
|
||||
|
||||
const clientLogos = [
|
||||
{ src: "/images/absa.png", alt: "Absa Bank Logo" }, // Replace with actual paths
|
||||
{ src: "/images/sybrin.svg", alt: "Sybrin Logo" },
|
||||
{ src: "/images/bcx.png", alt: "BCX Logo" },
|
||||
{ src: "/images/sasol.png", alt: "Sasol Logo" },
|
||||
{ src: "/images/toyota-logo.png", alt: "Toyota Logo" },
|
||||
];
|
||||
|
||||
// --- Page Component ---
|
||||
|
||||
export default function ResourceAugmentationPage() {
|
||||
return (
|
||||
<div className="bg-white text-gray-800 overflow-x-hidden dark:bg-gray-900 dark:text-gray-200 font-poppins">
|
||||
{/* 1. Hero Section */}
|
||||
<section className="relative bg-gradient-to-r from-gray-800 via-gray-700 to-gray-900 text-white py-20 md:py-32">
|
||||
<div className="absolute inset-0 bg-black opacity-50 dark:opacity-70"></div>
|
||||
{/* Optional: Add a subtle background pattern or image */}
|
||||
{/* <div className="absolute inset-0 bg-[url('/images/team-background.jpg')] bg-cover bg-center opacity-10"></div> */}
|
||||
<div className="container mx-auto px-6 text-center relative z-10">
|
||||
{/* Consider adding OMS Logo here if desired */}
|
||||
{/* <Image src="/oms-logo-white.png" alt="OMS Logo" width={150} height={50} className="mx-auto mb-6" /> */}
|
||||
<h1
|
||||
className="text-3xl md:text-5xl lg:text-6xl font-bold mb-4 font-poppins drop-shadow-md"
|
||||
style={{ color: COLORS.primary }} // Use gold color
|
||||
>
|
||||
Flexible IT Resource Augmentation & Managed Services
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl max-w-4xl mx-auto leading-relaxed text-gray-200 dark:text-gray-300 mb-8">
|
||||
Scale your IT capabilities effectively with Owethu Managed Services.
|
||||
Access expert resources on demand or entrust us with full project
|
||||
delivery through our flexible engagement models.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row justify-center items-center gap-4">
|
||||
<Link
|
||||
href="#service-models" // Link to the service models section
|
||||
className="inline-flex items-center justify-center bg-gold-500 text-gray-900 font-bold py-3 px-8 rounded-md hover:bg-gold-600 transition-colors duration-300"
|
||||
style={{ backgroundColor: COLORS.primary }}
|
||||
>
|
||||
Explore Our Models <FaArrowRight className="ml-2" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact?subject=Resource Augmentation Inquiry"
|
||||
className="inline-flex items-center text-gold-400 hover:text-gold-300 transition-colors duration-300 font-semibold"
|
||||
style={{ color: COLORS.primary }} // Use gold color for secondary CTA text too
|
||||
>
|
||||
Discuss Your Needs
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 2. The Need / Why Resource Augmentation? Section */}
|
||||
<section className="py-16 md:py-24 bg-gray-50 dark:bg-gray-800">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Bridge Your IT Skills Gap & Scale Efficiently
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
Modern IT projects demand diverse skills and flexible capacity.
|
||||
Resource augmentation helps overcome common challenges faced by
|
||||
businesses today.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
icon: FaPuzzlePiece,
|
||||
title: "Skill Gaps",
|
||||
desc: "Struggling to find specialized IT talent for specific project needs or new technologies.",
|
||||
},
|
||||
{
|
||||
icon: FaChartLine,
|
||||
title: "Fluctuating Demand",
|
||||
desc: "Need to scale development or testing teams quickly for peak periods without long-term overhead.",
|
||||
},
|
||||
{
|
||||
icon: FaClock,
|
||||
title: "Project Delays",
|
||||
desc: "Lack of internal resources causing bottlenecks and delaying critical project timelines.",
|
||||
},
|
||||
{
|
||||
icon: FaFileInvoiceDollar,
|
||||
title: "Cost Control",
|
||||
desc: "Wanting to manage IT personnel costs effectively, avoiding expensive recruitment and full-time hires.",
|
||||
},
|
||||
{
|
||||
icon: FaBriefcase,
|
||||
title: "Focus on Core Business",
|
||||
desc: "Needing to free up internal teams to concentrate on strategic goals rather than specific project tasks.",
|
||||
},
|
||||
{
|
||||
icon: FaSyncAlt,
|
||||
title: "Need for Flexibility",
|
||||
desc: "Requiring adaptable staffing solutions that can change as project requirements evolve.",
|
||||
},
|
||||
].map((item) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md text-center transform transition duration-300 hover:-translate-y-1 hover:shadow-lg"
|
||||
>
|
||||
<item.icon
|
||||
className="text-4xl text-red-500 dark:text-red-400 mx-auto mb-4" // Using red to signify challenges/needs
|
||||
/>
|
||||
<h4 className="text-xl font-semibold mb-2 font-poppins dark:text-white">
|
||||
{item.title}
|
||||
</h4>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm">
|
||||
{item.desc}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3. Our Solution: OMS Resource Augmentation */}
|
||||
<section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 font-poppins text-gray-900 dark:text-white leading-tight">
|
||||
Your Strategic Partner for IT Talent
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed mb-4">
|
||||
Owethu Managed Services (OMS) provides high-caliber IT
|
||||
professionals through flexible engagement models tailored to
|
||||
your specific project requirements and business objectives. As a
|
||||
100% Black female-owned organization based in Centurion,
|
||||
Gauteng, we are committed to excellence and delivering value.
|
||||
</p>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 leading-relaxed font-semibold">
|
||||
Whether you need individual experts to augment your team or a
|
||||
fully managed service to deliver key project milestones, OMS
|
||||
offers the expertise and flexibility you need to succeed. We are
|
||||
also a Salesforce Partner, providing licenses, implementation,
|
||||
and managed services.
|
||||
</p>
|
||||
<Link
|
||||
href="/about" // Link to your main About Us page
|
||||
className="inline-flex items-center mt-4 text-gold-600 dark:text-gold-400 hover:text-gold-700 dark:hover:text-gold-300 font-semibold transition-colors duration-300"
|
||||
style={{ color: COLORS.primary }}
|
||||
>
|
||||
Learn More About OMS <FaArrowRight className="ml-2" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="relative h-64 md:h-80 rounded-lg overflow-hidden shadow-lg">
|
||||
{/* Placeholder Image - Replace with a relevant image (e.g., team collaboration, office) */}
|
||||
<Image
|
||||
src="/images/team-collaborative.png" // Replace with actual path
|
||||
alt="OMS Team Collaboration"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="bg-gray-200 dark:bg-gray-700"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 4. Our Service Models Section */}
|
||||
<section
|
||||
id="service-models"
|
||||
className="py-16 md:py-24 bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-700"
|
||||
>
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Flexible Engagement Models
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
Choose the approach that best suits your project needs, management
|
||||
style, and budget.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto">
|
||||
{serviceModels.map((model) => (
|
||||
<div
|
||||
key={model.title}
|
||||
className="bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md flex flex-col"
|
||||
>
|
||||
<div className="flex items-center mb-4">
|
||||
<model.icon
|
||||
className="text-3xl mr-3"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h3 className="text-xl font-semibold font-poppins text-gray-900 dark:text-white">
|
||||
{model.title}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4 text-sm flex-grow">
|
||||
{model.description}
|
||||
</p>
|
||||
<ul className="space-y-2 mb-4">
|
||||
{model.details.map((detail, index) => (
|
||||
<li key={index} className="flex items-start text-sm">
|
||||
<FaCheckCircle className="w-4 h-4 text-green-500 dark:text-green-400 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{detail}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="text-sm font-semibold text-gray-800 dark:text-gray-200 mt-auto pt-4 border-t border-gray-200 dark:border-gray-600">
|
||||
{model.pricingModel}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-center text-md text-gray-700 dark:text-gray-300 mt-12 max-w-3xl mx-auto">
|
||||
OMS operates using both <span className="font-semibold">AGILE</span>{" "}
|
||||
and <span className="font-semibold">Waterfall</span> Frameworks,
|
||||
depending on client preference and project suitability.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 5. Capabilities & Expertise Section */}
|
||||
<section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<FaTools
|
||||
className="text-5xl mx-auto mb-4"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Our Core Capabilities
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
We provide skilled professionals across key IT disciplines at
|
||||
various experience levels (Junior, Intermediate, Senior).
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{capabilities.map((capability) => (
|
||||
<div
|
||||
key={capability.role}
|
||||
className="relative bg-gray-50 dark:bg-gray-800 p-6 rounded-lg shadow-sm border-t-4 dark:border-gold-400 transition-shadow hover:shadow-md"
|
||||
style={{ borderColor: COLORS.primary }}
|
||||
>
|
||||
<capability.icon
|
||||
className="text-3xl mb-3"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h4 className="text-lg font-semibold mb-2 font-poppins dark:text-white">
|
||||
{capability.role}
|
||||
</h4>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm leading-relaxed mb-3">
|
||||
{capability.description}
|
||||
</p>
|
||||
{/* Simplified skills display for the web page overview */}
|
||||
<h5 className="text-sm font-semibold mb-1 dark:text-gray-200">
|
||||
Key Areas of Expertise:
|
||||
</h5>
|
||||
<ul className="list-disc list-inside space-y-1 text-xs text-gray-600 dark:text-gray-300">
|
||||
{capability.skills[0].points.slice(0, 4).map(
|
||||
(
|
||||
point,
|
||||
index // Show first few points from senior/general list
|
||||
) => (
|
||||
<li key={index}>{point}</li>
|
||||
)
|
||||
)}
|
||||
{capability.skills[0].points.length > 4 && (
|
||||
<li>And more...</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-center text-md text-gray-700 dark:text-gray-300 mt-12 max-w-3xl mx-auto">
|
||||
Detailed capability breakdowns by seniority level (Junior,
|
||||
Intermediate, Senior) are available upon request.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 6. Benefits of Partnering with OMS Section */}
|
||||
<section className="py-16 md:py-24 bg-gray-50 dark:bg-gray-800">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<FaRegHandshake
|
||||
className="text-5xl mx-auto mb-4"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Why Choose OMS for Resource Augmentation?
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
Leverage our expertise and flexible models to gain a competitive
|
||||
advantage.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{augmentationBenefits.map((benefit) => (
|
||||
<div
|
||||
key={benefit.title}
|
||||
className="bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md text-center transform transition duration-300 hover:-translate-y-1 hover:shadow-lg"
|
||||
>
|
||||
<benefit.icon
|
||||
className="text-4xl mx-auto mb-4"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h4 className="text-lg font-semibold mb-2 font-poppins dark:text-white">
|
||||
{benefit.title}
|
||||
</h4>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-sm leading-relaxed">
|
||||
{benefit.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 7. Proven Experience / Past Projects (High Level) */}
|
||||
<section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-12 md:mb-16">
|
||||
<FaBusinessTime
|
||||
className="text-5xl mx-auto mb-4"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Proven Track Record Across Industries
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
We have successfully delivered resources and managed projects in
|
||||
complex environments, including:
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto text-center">
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-6 rounded-lg shadow-sm">
|
||||
<FaShieldAlt
|
||||
className="text-3xl mx-auto mb-3"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h4 className="text-lg font-semibold mb-2 font-poppins dark:text-white">
|
||||
Financial Services & Compliance
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
FIC/KYC Remediation (Data Architecture, MI), Core Banking
|
||||
Systems, Lending/Credit Process Optimisation.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-6 rounded-lg shadow-sm">
|
||||
<FaUsersCog
|
||||
className="text-3xl mx-auto mb-3"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h4 className="text-lg font-semibold mb-2 font-poppins dark:text-white">
|
||||
Customer Management & Onboarding
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Design, Testing, Requirements, Process Engineering, UX/UI for
|
||||
customer-facing platforms.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-6 rounded-lg shadow-sm">
|
||||
<FaSitemap
|
||||
className="text-3xl mx-auto mb-3"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h4 className="text-lg font-semibold mb-2 font-poppins dark:text-white">
|
||||
Platform & Process Automation
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Straight-Through Processing (STP) on Banking Platforms, API
|
||||
Integration, Salesforce implementations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Optional: Mention specific technologies briefly if desired */}
|
||||
{/* <p className="text-center text-md text-gray-500 dark:text-gray-400 mt-8 max-w-4xl mx-auto italic">
|
||||
Expertise includes: Salesforce, AWS, Azure, Data Warehousing, BI Tools, BPMN, Various Banking Systems (Sybrin, Flexicube, etc.), Automation Frameworks (Selenium), and more.
|
||||
</p> */}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 8. Implementation Approaches / Methodologies */}
|
||||
<section className="py-16 md:py-24 bg-gray-800 text-white">
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
<FaLayerGroup
|
||||
className="text-5xl mx-auto mb-4"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins mb-4">
|
||||
Adaptable Delivery Methodologies
|
||||
</h2>
|
||||
<p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed text-gray-300 mb-8">
|
||||
We are proficient in various project management frameworks and adapt
|
||||
our approach to align with your organizational standards and project
|
||||
needs.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center items-center gap-8">
|
||||
{methodologies.map((method) => (
|
||||
<div
|
||||
key={method.name}
|
||||
className="flex flex-col items-center p-4 bg-gray-700 rounded-lg w-48"
|
||||
>
|
||||
<method.icon
|
||||
className="text-4xl mb-2"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<span className="font-semibold text-gray-200">
|
||||
{method.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 9. Clients & Partners Section */}
|
||||
<section className="py-16 md:py-24 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6">
|
||||
<h2 className="text-center text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-12">
|
||||
Trusted by Leading Organizations
|
||||
</h2>
|
||||
{/* Updated the grid classes here */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-8 md:gap-12 items-center max-w-5xl mx-auto">
|
||||
{clientLogos.map((logo) => (
|
||||
<div
|
||||
key={logo.alt}
|
||||
className="relative h-16 md:h-20 filter grayscale hover:grayscale-0 transition duration-300 brightness-75 hover:brightness-100 dark:brightness-150 dark:hover:brightness-100 dark:invert dark:hover:invert-0"
|
||||
>
|
||||
<Image
|
||||
src={logo.src}
|
||||
alt={logo.alt}
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 10. Call to Action (CTA) Section */}
|
||||
<section
|
||||
className="py-16 md:py-24 bg-gold-500 text-gray-900"
|
||||
style={{ backgroundColor: COLORS.primary }}
|
||||
>
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4 font-poppins">
|
||||
Ready to Enhance Your IT Team?
|
||||
</h2>
|
||||
<p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed mb-8 font-poppins text-gray-800">
|
||||
Let's discuss how OMS resource augmentation or managed services
|
||||
can help you achieve your project goals. Contact us today for a
|
||||
consultation tailored to your specific needs.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row justify-center items-center gap-4">
|
||||
<Link
|
||||
href="/contact?subject=Resource Augmentation Consultation"
|
||||
className="inline-block bg-gray-800 text-white font-bold py-3 px-8 rounded-md hover:bg-gray-900 dark:bg-gray-900 dark:hover:bg-black transition-colors duration-300"
|
||||
>
|
||||
Request a Consultation
|
||||
</Link>
|
||||
<Link
|
||||
href="mailto:admin@oms.africa" // Direct email link
|
||||
className="inline-block bg-transparent border-2 border-gray-800 text-gray-800 font-bold py-3 px-8 rounded-md hover:bg-gray-800 hover:text-white transition-colors duration-300"
|
||||
>
|
||||
Email Us Directly
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 11. Contact Information Section */}
|
||||
<section className="py-16 md:py-24 bg-gray-100 dark:bg-gray-800">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Get in Touch
|
||||
</h2>
|
||||
<p className="text-md md:text-lg text-gray-700 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
We're ready to assist you. Reach out via phone, email, or use
|
||||
the form below to start the conversation about your IT resource
|
||||
needs.
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
|
||||
{/* Contact Details */}
|
||||
<div className="space-y-6 bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md">
|
||||
<h3 className="text-xl font-semibold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Contact Information
|
||||
</h3>
|
||||
<div className="flex items-center space-x-4">
|
||||
<FaPhone
|
||||
className="w-6 h-6 flex-shrink-0"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold font-poppins text-gray-800 dark:text-gray-100">
|
||||
Phone
|
||||
</h4>
|
||||
{/* Using specific numbers from PDF */}
|
||||
<a
|
||||
href="tel:+27120513281"
|
||||
className="block text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100 transition text-sm"
|
||||
>
|
||||
Zanele: (012) 051 3281
|
||||
</a>
|
||||
<a
|
||||
href="tel:+27120513282"
|
||||
className="block text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100 transition text-sm"
|
||||
>
|
||||
Lindiwe: (012) 051 3282
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<FaEnvelope
|
||||
className="w-6 h-6 flex-shrink-0"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold font-poppins text-gray-800 dark:text-gray-100">
|
||||
Email
|
||||
</h4>
|
||||
{/* Using specific emails from PDF */}
|
||||
<a
|
||||
href="mailto:Zanelem@oms.africa"
|
||||
className="block text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100 transition text-sm"
|
||||
>
|
||||
Zanelem@oms.africa
|
||||
</a>
|
||||
<a
|
||||
href="mailto:Lindiwes@oms.africa"
|
||||
className="block text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100 transition text-sm"
|
||||
>
|
||||
Lindiwes@oms.africa
|
||||
</a>
|
||||
<a
|
||||
href="mailto:admin@oms.africa"
|
||||
className="block text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100 transition text-sm mt-1"
|
||||
>
|
||||
admin@oms.africa (General)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/* Optional: Link to main contact page */}
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-flex items-center text-gold-600 dark:text-gold-400 hover:text-gold-700 dark:hover:text-gold-300 font-semibold transition-colors duration-300 mt-4"
|
||||
style={{ color: COLORS.primary }}
|
||||
>
|
||||
Go to Full Contact Page <FaArrowRight className="ml-2" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Contact Form */}
|
||||
<div className="bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md">
|
||||
<h3 className="text-xl font-semibold font-poppins text-gray-900 dark:text-white mb-4">
|
||||
Send a Quick Inquiry
|
||||
</h3>
|
||||
<ContactForm /> {/* Reuse your existing contact form */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
298
app/(website)/tech-talk/[slug]/page.tsx
Normal file
@ -0,0 +1,298 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import { getPostBySlug, getPosts } from "@/lib/query/post";
|
||||
import type { Metadata } from "next";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
ContentBlock,
|
||||
OutputData,
|
||||
ParagraphData,
|
||||
HeaderData,
|
||||
ListData,
|
||||
ImageData,
|
||||
QuoteData,
|
||||
CodeData,
|
||||
} from "@/types";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await getPosts();
|
||||
if (!Array.isArray(posts)) {
|
||||
console.error("getPosts did not return an array:", posts);
|
||||
return [];
|
||||
}
|
||||
return posts.map((post) => ({
|
||||
slug: post.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
function extractDescriptionFromBlocks(
|
||||
content: OutputData | null
|
||||
): string | null {
|
||||
if (
|
||||
!content ||
|
||||
!content.blocks ||
|
||||
!Array.isArray(content.blocks) ||
|
||||
content.blocks.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const firstParagraph = content.blocks.find(
|
||||
(block): block is ContentBlock & { data: ParagraphData } =>
|
||||
block.type === "paragraph"
|
||||
);
|
||||
if (firstParagraph?.data?.text) {
|
||||
const plainText = firstParagraph.data.text
|
||||
.replace(/<[^>]*>/g, "")
|
||||
.replace(/&[^;]+;/g, "");
|
||||
return plainText.substring(0, 160) + (plainText.length > 160 ? "..." : "");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const post = await getPostBySlug(slug, {
|
||||
fields: [
|
||||
"slug",
|
||||
"status",
|
||||
"user_created",
|
||||
"date_created",
|
||||
"user_updated",
|
||||
"date_updated",
|
||||
"title",
|
||||
"content",
|
||||
"excerpt",
|
||||
"featured_image",
|
||||
],
|
||||
});
|
||||
|
||||
if (!post) {
|
||||
return {
|
||||
title: "Post Not Found",
|
||||
};
|
||||
}
|
||||
|
||||
const imageUrl = post.featured_image;
|
||||
|
||||
const descriptionFromContent =
|
||||
post.content &&
|
||||
typeof post.content === "object" &&
|
||||
Array.isArray(post.content.blocks)
|
||||
? extractDescriptionFromBlocks(post.content as OutputData)
|
||||
: null;
|
||||
|
||||
const description =
|
||||
post.excerpt || descriptionFromContent || "Read this OMS TechTalk article.";
|
||||
|
||||
const publishedTime = post.date_created
|
||||
? new Date(post.date_created).toISOString()
|
||||
: "";
|
||||
|
||||
return {
|
||||
title: `${post.title} | OMS TechTalk`,
|
||||
description: description,
|
||||
alternates: {
|
||||
canonical: `/tech-talk/${post.slug}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: description,
|
||||
url: `https://oms.africa/tech-talk/${post.slug}`,
|
||||
type: "article",
|
||||
publishedTime: publishedTime,
|
||||
...(imageUrl && {
|
||||
images: [
|
||||
{
|
||||
url: imageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: post.title,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: post.title,
|
||||
description: description,
|
||||
...(imageUrl && { images: [imageUrl] }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const renderBlock = (block: ContentBlock) => {
|
||||
const renderListItems = (items: string[]) => {
|
||||
return items.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="mb-2"
|
||||
dangerouslySetInnerHTML={{ __html: item }}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
switch (block.type) {
|
||||
case "header":
|
||||
const headerBlock = block as ContentBlock & { data: HeaderData };
|
||||
const level = headerBlock.data.level || 2;
|
||||
// Ensure HeaderTag has a type that JSX understands for intrinsic elements.
|
||||
const HeaderTag = `h${level}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
||||
return (
|
||||
<HeaderTag
|
||||
key={block.id}
|
||||
className={`text-${
|
||||
6 - (headerBlock.data.level || 2)
|
||||
}xl font-bold my-4 text-primary`}
|
||||
dangerouslySetInnerHTML={{ __html: headerBlock.data.text }}
|
||||
/>
|
||||
);
|
||||
case "paragraph":
|
||||
const paragraphBlock = block as ContentBlock & { data: ParagraphData };
|
||||
return (
|
||||
<p
|
||||
key={block.id}
|
||||
className="my-4 leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraphBlock.data.text }}
|
||||
/>
|
||||
);
|
||||
case "list":
|
||||
const listBlock = block as ContentBlock & { data: ListData };
|
||||
const ListTag = listBlock.data.style === "ordered" ? "ol" : "ul";
|
||||
return (
|
||||
<ListTag
|
||||
key={block.id}
|
||||
className={`my-4 pl-6 ${
|
||||
listBlock.data.style === "ordered" ? "list-decimal" : "list-disc"
|
||||
}`}
|
||||
>
|
||||
{listBlock.data.items &&
|
||||
Array.isArray(listBlock.data.items) &&
|
||||
renderListItems(listBlock.data.items)}
|
||||
</ListTag>
|
||||
);
|
||||
case "image":
|
||||
const imageBlock = block as ContentBlock & { data: ImageData };
|
||||
const imageUrl = imageBlock.data.file?.url;
|
||||
|
||||
if (!imageUrl) return null;
|
||||
|
||||
return (
|
||||
<div key={block.id} className="my-6">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={imageBlock.data.caption || "Blog post image"}
|
||||
width={imageBlock.data.file?.width || 800}
|
||||
height={imageBlock.data.file?.height || 450}
|
||||
className="rounded-md shadow-sm mx-auto"
|
||||
/>
|
||||
{imageBlock.data.caption && (
|
||||
<p className="text-center text-sm text-muted-foreground mt-2">
|
||||
{imageBlock.data.caption}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
case "quote":
|
||||
const quoteBlock = block as ContentBlock & { data: QuoteData };
|
||||
return (
|
||||
<blockquote
|
||||
key={block.id}
|
||||
className="my-6 pl-4 border-l-4 border-primary italic text-muted-foreground"
|
||||
>
|
||||
<p dangerouslySetInnerHTML={{ __html: quoteBlock.data.text }} />
|
||||
{quoteBlock.data.caption && (
|
||||
<footer className="text-sm mt-2">
|
||||
- {quoteBlock.data.caption}
|
||||
</footer>
|
||||
)}
|
||||
</blockquote>
|
||||
);
|
||||
case "code":
|
||||
const codeBlock = block as ContentBlock & { data: CodeData };
|
||||
const codeContent = codeBlock.data.code || "";
|
||||
return (
|
||||
<pre
|
||||
key={block.id}
|
||||
className="my-6 p-4 bg-muted text-muted-foreground rounded-md overflow-x-auto text-sm"
|
||||
>
|
||||
<code>{codeContent}</code>
|
||||
</pre>
|
||||
);
|
||||
default:
|
||||
console.warn(`Unknown block type: ${block.type}`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default async function PostPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const post = await getPostBySlug(slug);
|
||||
|
||||
if (!post) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const contentBlocks =
|
||||
post.content &&
|
||||
typeof post.content === "object" &&
|
||||
Array.isArray(post.content.blocks)
|
||||
? (post.content.blocks as ContentBlock[])
|
||||
: null;
|
||||
|
||||
const coverImageUrl =
|
||||
post.featured_image + "?width=1200&height=600&quality=80&fit=cover";
|
||||
|
||||
return (
|
||||
<article className="container mx-auto px-4 py-8">
|
||||
<header className="text-center mb-12 max-w-3xl mx-auto">
|
||||
{coverImageUrl && (
|
||||
<div className="relative w-full h-64 md:h-96 mb-6 rounded-lg overflow-hidden">
|
||||
<Image
|
||||
src={coverImageUrl}
|
||||
alt={`Cover image for ${post.title}`}
|
||||
fill={true}
|
||||
style={{ objectFit: "cover" }}
|
||||
className="shadow-md"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<h1 className="text-4xl md:text-5xl font-extrabold leading-tight mb-4 text-primary">
|
||||
{post.title}
|
||||
</h1>
|
||||
{post.date_created && (
|
||||
<p className="text-muted-foreground text-sm mb-2">
|
||||
Published on {format(new Date(post.date_created), "MMMM dd, yyyy")}
|
||||
</p>
|
||||
)}
|
||||
{post.excerpt && (
|
||||
<p className="text-lg text-muted-foreground italic mt-4">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-4 max-w-3xl">
|
||||
<div
|
||||
className="prose prose-lg lg:prose-xl dark:prose-invert max-w-none mx-auto
|
||||
prose-headings:text-primary prose-a:text-blue-600 hover:prose-a:text-blue-800 dark:prose-a:text-blue-400 dark:hover:prose-a:text-blue-300
|
||||
prose-strong:font-semibold prose-img:rounded-md prose-img:shadow-sm"
|
||||
>
|
||||
{contentBlocks && contentBlocks.length > 0 ? (
|
||||
contentBlocks.map((block) => renderBlock(block))
|
||||
) : (
|
||||
<p>Content is not available or in an unexpected format.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@ -1,39 +1,8 @@
|
||||
import React from "react";
|
||||
import BlogPostCard from "@/components/BlogPostCard";
|
||||
import { prisma } from "@/lib/prisma"; // Import Prisma client
|
||||
import { auth } from "@/auth"; // Import auth to check session
|
||||
import Button from "@/components/ui/Button"; // Import Button component
|
||||
import type { Metadata } from "next";
|
||||
|
||||
interface Post {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
content: string;
|
||||
excerpt?: string | null;
|
||||
imageUrl?: string | null;
|
||||
published: boolean;
|
||||
authorId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
// --- Fetch Posts ---
|
||||
async function getPublishedPosts() {
|
||||
try {
|
||||
const posts = await prisma.post.findMany({
|
||||
where: { published: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
// select needed fields if not all
|
||||
});
|
||||
return posts;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch posts:", error);
|
||||
return []; // Return empty array on error
|
||||
}
|
||||
}
|
||||
|
||||
import { getPosts } from "@/lib/query/post";
|
||||
import { Post } from "@/types";
|
||||
// --- SEO Metadata ---
|
||||
export const metadata: Metadata = {
|
||||
title: "OMS TechTalk | Insights & Innovation",
|
||||
@ -65,8 +34,7 @@ export const metadata: Metadata = {
|
||||
|
||||
// --- Page Component ---
|
||||
const TechTalkPage = async () => {
|
||||
const posts = await getPublishedPosts();
|
||||
const session = await auth(); // Get session info
|
||||
const posts: Post[] = await getPosts();
|
||||
|
||||
return (
|
||||
<div className="bg-background text-foreground">
|
||||
@ -80,28 +48,21 @@ const TechTalkPage = async () => {
|
||||
Insights, trends, and discussions on the latest in technology,
|
||||
innovation, and digital transformation from the experts at OMS.
|
||||
</p>
|
||||
{/* Conditionally render Create Post button */}
|
||||
{session?.user && (
|
||||
<div className="mt-8">
|
||||
<Button href="/tech-talk/create" variant="primary">
|
||||
Create New Post
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Blog Post Grid */}
|
||||
{posts.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-10">
|
||||
{posts.map((post: Post) => (
|
||||
<BlogPostCard
|
||||
key={post.id} // Use post ID as key
|
||||
key={post.slug}
|
||||
slug={post.slug}
|
||||
title={post.title}
|
||||
excerpt={post.excerpt ?? post.content.substring(0, 150) + "..."}
|
||||
// Use imageUrl from DB or a default placeholder
|
||||
imageUrl={post.imageUrl ?? "/posts/default-placeholder.jpg"} // Provide a default image
|
||||
author={"OMS Team"} // Replace with actual author logic if available (e.g., post.user.name)
|
||||
date={new Date(post.createdAt).toLocaleDateString("en-US", {
|
||||
excerpt={post.excerpt ?? "No excerpt available"}
|
||||
imageUrl={
|
||||
post.featured_image ?? "/posts/default-placeholder.jpg"
|
||||
}
|
||||
author={"OMS Team"}
|
||||
date={new Date(post.date_created).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
|
||||
54
app/(website)/vacancies/[slug]/page.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
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<ExtendedVacancy | null> {
|
||||
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();
|
||||
}
|
||||
|
||||
export default async function VacancyDetailsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const vacancy = await getVacancy(slug);
|
||||
|
||||
if (!vacancy) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const shareUrl = `${process.env.WEBSITE_URL}/vacancies/${slug}`;
|
||||
const shareTitle = encodeURIComponent(
|
||||
`Job Opening: ${vacancy.title} at ${
|
||||
vacancy.company?.name || "Owethu Managed Services"
|
||||
}`
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-black text-gray-800 font-poppins overflow-x-hidden min-h-screen py-12 md:py-5">
|
||||
<VacancyClientContent
|
||||
vacancy={vacancy}
|
||||
shareUrl={shareUrl}
|
||||
shareTitle={shareTitle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
app/(website)/vacancies/_components/Badge.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
export const Badge = ({
|
||||
children,
|
||||
icon: Icon,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
icon?: React.ElementType;
|
||||
}) => (
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-200 font-poppins">
|
||||
{Icon && <Icon className="h-3 w-3" />}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
31
app/(website)/vacancies/_components/ListSection.tsx
Normal file
@ -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 (
|
||||
<div className="mb-8">
|
||||
<h3 className="flex items-center gap-2 text-xl font-semibold mb-3 text-gray-900 dark:text-white font-poppins border-b border-gray-200 pb-2">
|
||||
{Icon && (
|
||||
<Icon
|
||||
className="h-5 w-5 text-primary"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
)}
|
||||
{title}
|
||||
</h3>
|
||||
<ul className="list-disc space-y-2 pl-6 text-gray-700 dark:text-white font-poppins text-sm leading-relaxed">
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
22
app/(website)/vacancies/_components/MetadataItem.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { COLORS } from "@/constants";
|
||||
|
||||
export const MetadataItem = ({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
}) => (
|
||||
<div className="flex items-start space-x-2 text-sm text-gray-700 dark:text-white font-poppins">
|
||||
<Icon
|
||||
className="h-5 w-5 mt-0.5 flex-shrink-0 text-primary"
|
||||
aria-hidden="true"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
<div>
|
||||
<span className="font-semibold">{label}:</span> {value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
349
app/(website)/vacancies/_components/VacancyClientContent.tsx
Normal file
@ -0,0 +1,349 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useState /* useRef, useEffect - Remove these if no longer needed */,
|
||||
} from "react"; // Updated imports
|
||||
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"; // Assuming you have a Button component
|
||||
import Modal from "@/components/ui/Modal";
|
||||
|
||||
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) {
|
||||
// State to control modal visibility
|
||||
const [isApplyModalOpen, setIsApplyModalOpen] = useState(false);
|
||||
// const applyFormRef = useRef<HTMLDivElement>(null); // Remove this ref
|
||||
// Remove the useEffect for scrolling
|
||||
|
||||
const handleOpenApplyModal = () => {
|
||||
setIsApplyModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseApplyModal = () => {
|
||||
setIsApplyModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{" "}
|
||||
{/* Adjusted padding */}
|
||||
{/* --- Company Header --- */}
|
||||
{vacancy.company && (
|
||||
<div className="mb-10 flex flex-col sm:flex-row items-center justify-between gap-6 p-6 bg-gray-50 dark:bg-gray-800 shadow-md rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-4 flex-grow min-w-0">
|
||||
{" "}
|
||||
{/* Added flex-grow and min-w-0 for wrapping */}
|
||||
{vacancy.company.logoUrl ? (
|
||||
<Image
|
||||
src={vacancy.company.logoUrl}
|
||||
alt={`${vacancy.company.name} Logo`}
|
||||
width={64} // Add width/height for better layout shift prevention
|
||||
height={64}
|
||||
className="h-16 w-16 object-contain rounded-md flex-shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-16 w-16 bg-gray-200 dark:bg-gray-700 rounded-md flex items-center justify-center flex-shrink-0">
|
||||
<FaBuilding
|
||||
className="h-8 w-8 text-primary"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
{" "}
|
||||
{/* Added min-w-0 here too */}
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white truncate">
|
||||
{" "}
|
||||
{/* Consider truncate */}
|
||||
{vacancy.title}
|
||||
</h1>
|
||||
<p className="text-lg text-gray-700 dark:text-gray-300">
|
||||
at {vacancy.company.name}
|
||||
</p>
|
||||
{vacancy.company.websiteUrl && (
|
||||
<Link
|
||||
href={vacancy.company.websiteUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 inline-flex items-center gap-1 group mt-1"
|
||||
>
|
||||
Visit website{" "}
|
||||
<FaLink className="h-4 w-4 transition-transform duration-200 group-hover:translate-x-1" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Apply Button in Header */}
|
||||
<Button
|
||||
variant="primary" // Use your Button component if available
|
||||
onClick={handleOpenApplyModal}
|
||||
className="flex-shrink-0 whitespace-nowrap" // Prevent shrinking/wrapping
|
||||
>
|
||||
Apply Now
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{/* --- End Company Header --- */}
|
||||
{/* --- Main Grid Layout --- */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 lg:gap-10">
|
||||
{/* --- Main Content Area (Left Column) --- */}
|
||||
<div className="lg:col-span-2 bg-white dark:bg-gray-800 shadow-lg rounded-lg p-6 md:p-8 border border-gray-200 dark:border-gray-700">
|
||||
{/* Title if no company header */}
|
||||
{!vacancy.company && (
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white">
|
||||
{vacancy.title}
|
||||
</h1>
|
||||
{/* Apply Button if no header */}
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleOpenApplyModal}
|
||||
className="ml-4 flex-shrink-0 whitespace-nowrap"
|
||||
>
|
||||
Apply Now
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Job Description */}
|
||||
<div className="mb-8">
|
||||
<h3 className="flex items-center gap-2 text-xl font-semibold mb-3 text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
<FaInfoCircle
|
||||
className="h-5 w-5 text-primary"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>{" "}
|
||||
Job Description
|
||||
</h3>
|
||||
<div
|
||||
className="prose prose-sm sm:prose-base max-w-none text-gray-700 dark:text-gray-300 font-poppins leading-relaxed prose-headings:text-gray-900 prose-headings:dark:text-white prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-strong:text-gray-800 dark:prose-strong:text-gray-200"
|
||||
dangerouslySetInnerHTML={{ __html: vacancy.description || "" }} // Use dangerouslySetInnerHTML if description is HTML
|
||||
>
|
||||
{/* Or render as text if plain: <p>{vacancy.description}</p> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* --- List Sections (Responsibilities, Qualifications, Skills, Benefits) --- */}
|
||||
{/* Assuming ListSection renders conditionally if items are empty */}
|
||||
<ListSection
|
||||
title="Responsibilities"
|
||||
items={vacancy.responsibilities}
|
||||
icon={FaListUl}
|
||||
/>
|
||||
<ListSection
|
||||
title="Required Qualifications"
|
||||
items={vacancy.requiredQualifications}
|
||||
icon={FaStar}
|
||||
/>
|
||||
<ListSection
|
||||
title="Preferred Qualifications"
|
||||
items={vacancy.preferredQualifications}
|
||||
icon={FaStar}
|
||||
/>
|
||||
|
||||
{/* Skills Section */}
|
||||
{vacancy.skills && vacancy.skills.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<h3 className="flex items-center gap-2 text-xl font-semibold mb-4 text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
<FaTools
|
||||
className="h-5 w-5 text-primary"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>{" "}
|
||||
Skills
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{vacancy.skills.map((skill, index) => (
|
||||
<Badge key={index}>{skill}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Benefits Section */}
|
||||
{vacancy.benefits && vacancy.benefits.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<h3 className="flex items-center gap-2 text-xl font-semibold mb-3 text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||
<FaGift
|
||||
className="h-5 w-5 text-primary"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>{" "}
|
||||
Benefits
|
||||
</h3>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2 text-gray-700 dark:text-gray-300 mt-3 text-sm">
|
||||
{vacancy.benefits.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="flex items-center space-x-2 font-poppins"
|
||||
>
|
||||
<FaCheckCircle className="h-4 w-4 text-green-600 flex-shrink-0" />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{/* --- End List Sections --- */}
|
||||
|
||||
{/* Apply button below main content (redundant if header button exists, keep or remove as needed) */}
|
||||
<div className="mt-10 text-center lg:text-left">
|
||||
<Button variant="primary" onClick={handleOpenApplyModal}>
|
||||
Apply for this Position
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* --- End Main Content Area --- */}
|
||||
|
||||
{/* --- Sidebar (Right Column) --- */}
|
||||
<div className="lg:col-span-1 space-y-8">
|
||||
{/* Metadata Card */}
|
||||
<div className="bg-gray-50 dark:bg-gray-800 shadow-md rounded-lg p-6 border-l-4 border-primary dark:border-gold-500">
|
||||
<h3 className="text-xl font-semibold mb-5 text-gray-900 dark:text-white font-poppins">
|
||||
Job Overview
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<MetadataItem
|
||||
icon={FaBuilding}
|
||||
label="Department"
|
||||
value={vacancy.department}
|
||||
/>
|
||||
<MetadataItem
|
||||
icon={FaMapMarkerAlt}
|
||||
label="Location"
|
||||
value={
|
||||
<>
|
||||
{vacancy.location.city}, {vacancy.location.country}{" "}
|
||||
{vacancy.location.remote && <Badge>Remote Possible</Badge>}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<MetadataItem
|
||||
icon={FaBriefcase}
|
||||
label="Type"
|
||||
value={<Badge>{vacancy.employmentType}</Badge>}
|
||||
/>
|
||||
<MetadataItem
|
||||
icon={FaGraduationCap}
|
||||
label="Level"
|
||||
value={vacancy.experienceLevel}
|
||||
/>
|
||||
{vacancy.salary && (
|
||||
<MetadataItem
|
||||
icon={FaDollarSign}
|
||||
label="Salary"
|
||||
value={
|
||||
<span className="font-semibold text-gray-800 dark:text-gray-200">
|
||||
{vacancy.salary.min.toLocaleString()} -{" "}
|
||||
{vacancy.salary.max.toLocaleString()}{" "}
|
||||
{vacancy.salary.currency} {vacancy.salary.period}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<MetadataItem
|
||||
icon={FaCalendarAlt}
|
||||
label="Posted"
|
||||
value={formatDate(vacancy.postedDate)}
|
||||
/>
|
||||
{vacancy.applicationDeadline && (
|
||||
<MetadataItem
|
||||
icon={FaClock}
|
||||
label="Apply by"
|
||||
value={formatDate(vacancy.applicationDeadline)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Share Card */}
|
||||
<div className="bg-gray-50 dark:bg-gray-800 shadow-md rounded-lg p-6 border-l-4 border-primary dark:border-gold-500">
|
||||
<h3 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white flex items-center gap-2 font-poppins">
|
||||
<FaShareAlt
|
||||
className="h-5 w-5 text-primary"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>{" "}
|
||||
Share this opening
|
||||
</h3>
|
||||
<div className="flex space-x-3 text-sm font-poppins">
|
||||
<a
|
||||
href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(
|
||||
shareUrl
|
||||
)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
LinkedIn
|
||||
</a>
|
||||
<a
|
||||
href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(
|
||||
shareUrl
|
||||
)}&text=${shareTitle}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:underline dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
Twitter
|
||||
</a>
|
||||
<a
|
||||
href={`mailto:?subject=${shareTitle}&body=Check out this job opening: ${encodeURIComponent(
|
||||
shareUrl
|
||||
)}`}
|
||||
className="text-gray-600 hover:underline dark:text-gray-400 dark:hover:text-gray-300"
|
||||
>
|
||||
Email
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={isApplyModalOpen}
|
||||
onClose={handleCloseApplyModal}
|
||||
title={`Apply for: ${vacancy.title}`}
|
||||
size="4xl"
|
||||
>
|
||||
<VacancyApplicationForm
|
||||
vacancyId={vacancy.id}
|
||||
vacancyTitle={vacancy.title}
|
||||
onClose={handleCloseApplyModal}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
236
app/(website)/vacancies/page.tsx
Normal file
@ -0,0 +1,236 @@
|
||||
"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.",
|
||||
};
|
||||
*/
|
||||
|
||||
// --- 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 (
|
||||
<Link
|
||||
href={`/vacancies/${vacancy.slug}`}
|
||||
className="group block w-full transform transition duration-300 ease-in-out hover:-translate-y-1" // Add subtle lift on hover
|
||||
>
|
||||
<div
|
||||
className="relative flex flex-col h-full overflow-hidden rounded-lg bg-white dark:bg-gray-800 p-6 shadow-md transition-shadow duration-300 hover:shadow-xl border-l-4 dark:border-l-yellow-500" // Card base style + left border + dark mode
|
||||
style={{ borderColor: COLORS.primary }} // Apply gold border color (consider dark mode alternative if needed)
|
||||
>
|
||||
<div className="flex-grow">
|
||||
<h3 className="mb-2 text-xl font-bold font-poppins text-gray-900 dark:text-gray-100 transition-colors group-hover:text-gray-700 dark:group-hover:text-gray-300">
|
||||
{vacancy.title}
|
||||
</h3>
|
||||
<div className="mb-4 flex flex-col space-y-2 text-sm text-gray-600 dark:text-gray-400 font-poppins">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<FaMapMarkerAlt
|
||||
className="h-4 w-4 flex-shrink-0"
|
||||
style={{ color: COLORS.primary }} // Keep gold or use dark:text-yellow-400
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>
|
||||
{vacancy.location.city}{" "}
|
||||
{vacancy.location.remote && (
|
||||
<span className="ml-1 rounded bg-gray-200 dark:bg-gray-700 px-1.5 py-0.5 text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
Remote
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<FaBriefcase
|
||||
className="h-4 w-4 flex-shrink-0"
|
||||
style={{ color: COLORS.primary }} // Keep gold or use dark:text-yellow-400
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{vacancy.employmentType}
|
||||
</span>
|
||||
{vacancy.postedDate && (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<FaRegClock
|
||||
className="h-4 w-4 flex-shrink-0"
|
||||
style={{ color: COLORS.primary }} // Keep gold or use dark:text-yellow-400
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Posted: {postedDate}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto pt-4 border-t border-gray-100 dark:border-gray-700">
|
||||
<span
|
||||
className="inline-flex items-center text-sm font-medium font-poppins"
|
||||
style={{ color: COLORS.primary }} // Keep gold or use dark:text-yellow-400
|
||||
>
|
||||
View Details
|
||||
<FaArrowRight
|
||||
className="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
// --- 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 (
|
||||
<div className="bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-200 overflow-x-hidden font-poppins">
|
||||
{/* Section 1: Hero / Page Header */}
|
||||
<section className="relative bg-gradient-to-r from-gray-800 via-gray-700 to-gray-800 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 text-white py-20 md:py-28">
|
||||
<div className="absolute inset-0 bg-black opacity-30 dark:opacity-50"></div>
|
||||
<div className="container mx-auto px-6 text-center relative z-10">
|
||||
<h1
|
||||
className="text-4xl md:text-5xl lg:text-6xl font-bold mb-4 font-poppins drop-shadow-md"
|
||||
style={{ color: COLORS.primary }} // Keep gold or use dark:text-yellow-400
|
||||
>
|
||||
Career Opportunities
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed text-gray-200 dark:text-gray-300 font-poppins">
|
||||
Join our team of innovators and experts. Explore current openings at
|
||||
OMS or submit your CV for future consideration.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Section 2: Vacancy List */}
|
||||
<section className="py-16 md:py-24">
|
||||
<div className="container mx-auto px-6">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-gray-100 mb-12 text-center">
|
||||
Current Openings
|
||||
</h2>
|
||||
{vacancies.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{vacancies.map((vacancy) => (
|
||||
<VacancyCard key={vacancy.id} vacancy={vacancy} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-10 max-w-2xl mx-auto rounded-lg border border-dashed border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 p-10 text-center shadow-sm">
|
||||
<FaSearch
|
||||
className="mx-auto mb-5 h-12 w-12"
|
||||
style={{ color: COLORS.primary }} // Keep gold or use dark:text-yellow-400
|
||||
/>
|
||||
<h3 className="text-xl font-semibold text-gray-800 dark:text-gray-100 font-poppins mb-2">
|
||||
No Open Vacancies Right Now
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 font-poppins">
|
||||
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!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Section 3: Future Positions / CV Submission */}
|
||||
<section
|
||||
className="py-16 md:py-24 text-gray-900 dark:text-gray-800" // Adjust text color for dark mode contrast on gold bg
|
||||
style={{ backgroundColor: COLORS.primary }} // Keep gold background
|
||||
>
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
<FaPaperPlane className="text-5xl mx-auto mb-5 text-gray-800 dark:text-gray-900" />{" "}
|
||||
{/* Ensure icon contrast */}
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4 font-poppins text-gray-900 dark:text-gray-900">
|
||||
{" "}
|
||||
{/* Ensure heading contrast */}
|
||||
Don't See the Right Fit?
|
||||
</h2>
|
||||
<p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed font-poppins text-gray-800 dark:text-gray-800">
|
||||
{" "}
|
||||
{/* 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.
|
||||
</p>
|
||||
<div className="mt-10">
|
||||
{/* --- Updated Button --- */}
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleOpenFuturePositionModal}
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="inline-flex items-center gap-2 group font-poppins bg-gray-800 text-black hover:bg-gray-900 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200" // Adjusted dark mode button colors
|
||||
>
|
||||
Submit Your CV
|
||||
<FaArrowRight className="h-4 w-4 transition-transform duration-200 group-hover:translate-x-1" />
|
||||
</Button>
|
||||
{/* --- End Updated Button --- */}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- Modal for Future Position Application --- */}
|
||||
<Modal
|
||||
isOpen={isFuturePositionModalOpen}
|
||||
onClose={handleCloseFuturePositionModal}
|
||||
title="Apply for Future Positions"
|
||||
size="4xl"
|
||||
>
|
||||
<VacancyApplicationForm
|
||||
vacancyId="future-position"
|
||||
vacancyTitle="General Application / Future Position"
|
||||
onClose={handleCloseFuturePositionModal}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
158
app/achievements/_data/achievementsDefaults.ts
Normal file
@ -0,0 +1,158 @@
|
||||
// /app/achievements/_data/achievementsDefaults.ts (or similar)
|
||||
|
||||
export interface Award {
|
||||
id: string | number;
|
||||
name: string;
|
||||
year: number | string;
|
||||
awardingBody: string;
|
||||
description?: string;
|
||||
imageUrl?: string; // URL for an award logo or image
|
||||
}
|
||||
|
||||
export interface Certification {
|
||||
id: string | number;
|
||||
name: string;
|
||||
issuingBody: string;
|
||||
logoUrl?: string;
|
||||
details?: string;
|
||||
}
|
||||
|
||||
export interface Partner {
|
||||
id: string | number;
|
||||
name: string;
|
||||
logoUrl: string; // Partner logo is usually essential
|
||||
websiteUrl?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface GalleryImage {
|
||||
id: string | number;
|
||||
imageUrl: string;
|
||||
caption?: string;
|
||||
alt: string;
|
||||
event?: string; // Optional: Name of the event
|
||||
date?: string; // Optional: Date of the event/photo
|
||||
}
|
||||
|
||||
export interface Milestone {
|
||||
id: string | number;
|
||||
description: string;
|
||||
date: string; // e.g., "Q4 2023", "June 2022"
|
||||
}
|
||||
|
||||
// --- Default Placeholder Data ---
|
||||
|
||||
export const defaultAwards: Award[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Top BEE Level 1 Contributor",
|
||||
year: 2023,
|
||||
awardingBody: "SANAS Accreditation Body",
|
||||
description:
|
||||
"Recognized for commitment to Broad-Based Black Economic Empowerment.",
|
||||
imageUrl: "/images/awards/bee-level1.png",
|
||||
}, // Replace with actual paths
|
||||
{
|
||||
id: 2,
|
||||
name: "Supplier Development Excellence",
|
||||
year: 2022,
|
||||
awardingBody: "Absa Enterprise Development",
|
||||
description:
|
||||
"Acknowledged for impactful participation in the Absa supplier development program.",
|
||||
imageUrl: "/images/awards/absa-supplier.png",
|
||||
},
|
||||
// Add more awards...
|
||||
];
|
||||
|
||||
export const defaultCertifications: Certification[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Salesforce Certified Partner",
|
||||
issuingBody: "Salesforce",
|
||||
logoUrl: "/images/certs/salesforce-partner.png",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "ISO 9001:2015 (Pending/In Progress)",
|
||||
issuingBody: "Relevant Body",
|
||||
details:
|
||||
"Actively working towards certification for quality management systems.",
|
||||
}, // Example of in-progress
|
||||
// Add more certifications
|
||||
];
|
||||
|
||||
export const defaultPartners: Partner[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Absa Bank",
|
||||
logoUrl: "/clients/absa-logo.png",
|
||||
websiteUrl: "https://www.absa.co.za/",
|
||||
}, // Using client logos for partners
|
||||
{
|
||||
id: 2,
|
||||
name: "Salesforce",
|
||||
logoUrl: "/images/certs/salesforce-partner.png",
|
||||
websiteUrl: "https://www.salesforce.com/",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Sybrin",
|
||||
logoUrl: "/clients/sybrin-logo.png",
|
||||
websiteUrl: "https://www.sybrin.com/",
|
||||
},
|
||||
// Add other key strategic partners if distinct from clients
|
||||
];
|
||||
|
||||
export const defaultGalleryImages: GalleryImage[] = [
|
||||
{
|
||||
id: 1,
|
||||
imageUrl: "/images/gallery/team-event-1.jpg",
|
||||
caption: "Annual Team Building Day 2023",
|
||||
alt: "OMS Team at Annual Event",
|
||||
event: "Team Building",
|
||||
date: "Nov 2023",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
imageUrl: "/images/gallery/award-ceremony-1.jpg",
|
||||
caption: "Receiving the Supplier Development Award",
|
||||
alt: "OMS receiving award",
|
||||
event: "Absa Awards Night",
|
||||
date: "Oct 2022",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
imageUrl: "/images/gallery/office-launch.jpg",
|
||||
caption: "Celebrating our new Centurion office opening",
|
||||
alt: "OMS new office launch",
|
||||
event: "Office Launch",
|
||||
date: "May 2023",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
imageUrl: "/images/gallery/client-workshop.jpg",
|
||||
caption: "Collaborative workshop session with a key client",
|
||||
alt: "OMS client workshop",
|
||||
event: "Client Workshop",
|
||||
date: "Sep 2023",
|
||||
},
|
||||
// Add more gallery images
|
||||
];
|
||||
|
||||
export const defaultMilestones: Milestone[] = [
|
||||
{ id: 1, description: "Established Owethu Managed Services", date: "2019" }, // Adjust start date if needed
|
||||
{
|
||||
id: 2,
|
||||
description: "Joined Absa Supplier Development Programme",
|
||||
date: "2020",
|
||||
},
|
||||
{ id: 3, description: "Became an official Salesforce Partner", date: "2022" },
|
||||
{ id: 4, description: "Launched the OBSE Product", date: "Q1 2023" },
|
||||
{ id: 5, description: "Achieved BEE Level 1 Status", date: "Q3 2023" },
|
||||
{
|
||||
id: 6,
|
||||
description: "Opened new Head Office in Centurion",
|
||||
date: "May 2023",
|
||||
},
|
||||
// Add more significant milestones
|
||||
];
|
||||
361
app/achievements/page.tsx
Normal file
@ -0,0 +1,361 @@
|
||||
// /app/achievements/page.tsx
|
||||
import Image from "next/image";
|
||||
import { Metadata } from "next";
|
||||
import {
|
||||
FaAward,
|
||||
FaCertificate,
|
||||
FaHandshake,
|
||||
FaCamera,
|
||||
FaTrophy,
|
||||
FaStar,
|
||||
FaExternalLinkAlt,
|
||||
} from "react-icons/fa";
|
||||
import { COLORS } from "@/constants"; // Assuming COLORS is available
|
||||
// import HeroSection from "../_components/HeroSection"; // Reuse HeroSection component
|
||||
|
||||
// Import the placeholder data
|
||||
import {
|
||||
defaultAwards,
|
||||
defaultCertifications,
|
||||
defaultPartners,
|
||||
defaultGalleryImages,
|
||||
defaultMilestones,
|
||||
} from "./_data/achievementsDefaults"; // Adjust path as needed
|
||||
import CallToActionSection from "../(website)/_components/CallToActionSection";
|
||||
|
||||
// SEO Metadata
|
||||
export const metadata: Metadata = {
|
||||
title: "Achievements & Recognition | Owethu Managed Services (OMS)",
|
||||
description:
|
||||
"Explore the awards, certifications, partnerships, and milestones achieved by OMS. See our commitment to excellence and empowerment.",
|
||||
keywords: [
|
||||
"OMS achievements",
|
||||
"Owethu Managed Services awards",
|
||||
"BEE Level 1",
|
||||
"Salesforce Partner",
|
||||
"company milestones",
|
||||
"IT company recognition",
|
||||
"supplier development program",
|
||||
"South Africa IT awards",
|
||||
"OMS gallery",
|
||||
"OMS partnerships",
|
||||
],
|
||||
openGraph: {
|
||||
title: "Achievements & Recognition | OMS",
|
||||
description:
|
||||
"Discover OMS's journey of success, awards, and key partnerships.",
|
||||
url: "https://oms.cvevolve.com/achievements", // Update with the final URL
|
||||
// Add an appropriate OG image URL if available
|
||||
// images: [{ url: '/images/oms-achievements-og.png', width: 1200, height: 630, alt: 'OMS Achievements' }],
|
||||
locale: "en_ZA",
|
||||
type: "website",
|
||||
},
|
||||
// Add twitter card if desired
|
||||
};
|
||||
|
||||
export default function AchievementsPage() {
|
||||
// In a real app, fetch data here using functions like getAchievements(), getAwards(), etc.
|
||||
const awards = defaultAwards;
|
||||
const certifications = defaultCertifications;
|
||||
const partners = defaultPartners;
|
||||
const galleryImages = defaultGalleryImages;
|
||||
const milestones = defaultMilestones;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 1. Hero Section */}
|
||||
{/* <HeroSection
|
||||
title={
|
||||
<>
|
||||
Our Journey & <br className="hidden md:block" />
|
||||
Recognition
|
||||
</>
|
||||
}
|
||||
subtitle="Celebrating our milestones, awards, and the partnerships that drive our success."
|
||||
// Optional button:
|
||||
// buttonText="Explore Our Services"
|
||||
// buttonHref="/services"
|
||||
imageUrl="/images/hero/achievements-hero.jpg" // Replace with a suitable hero image (e.g., abstract success, team celebration)
|
||||
/> */}
|
||||
|
||||
{/* 2. Introduction (Optional) */}
|
||||
<section className="py-12 md:py-16 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6 text-center">
|
||||
<p className="text-lg md:text-xl text-gray-700 dark:text-gray-300 max-w-3xl mx-auto leading-relaxed">
|
||||
At Owethu Managed Services, we are proud of the progress we've
|
||||
made and the recognition we've earned. These achievements
|
||||
reflect our commitment to excellence, empowerment, and delivering
|
||||
value to our clients and community.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3. Awards Section */}
|
||||
{awards && awards.length > 0 && (
|
||||
<section className="py-16 md:py-20 bg-gray-50 dark:bg-gray-800">
|
||||
<div className="container mx-auto px-6">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-center text-gray-900 dark:text-white mb-12">
|
||||
<FaAward
|
||||
className="inline-block mr-3 mb-1"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>{" "}
|
||||
Awards & Accolades
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{awards.map((award) => (
|
||||
<div
|
||||
key={award.id}
|
||||
className="bg-white dark:bg-gray-700 p-6 rounded-lg shadow-md flex flex-col items-center text-center transform transition duration-300 hover:-translate-y-1 hover:shadow-lg"
|
||||
>
|
||||
{award.imageUrl && (
|
||||
<div className="relative h-20 w-20 mb-4">
|
||||
<Image
|
||||
src={award.imageUrl}
|
||||
alt={`${award.name} Logo`}
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!award.imageUrl && (
|
||||
<FaTrophy
|
||||
className="text-4xl mb-4"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
)}
|
||||
<h3 className="text-xl font-semibold mb-1 font-poppins text-gray-900 dark:text-white">
|
||||
{award.name}
|
||||
</h3>
|
||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
{award.awardingBody} - {award.year}
|
||||
</p>
|
||||
{award.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{award.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 4. Certifications & Partnerships Section */}
|
||||
{((certifications && certifications.length > 0) ||
|
||||
(partners && partners.length > 0)) && (
|
||||
<section className="py-16 md:py-20 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-center text-gray-900 dark:text-white mb-12">
|
||||
<FaCertificate
|
||||
className="inline-block mr-3 mb-1"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>{" "}
|
||||
Certifications &{" "}
|
||||
<FaHandshake
|
||||
className="inline-block ml-2 mr-3 mb-1"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>{" "}
|
||||
Partnerships
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-16">
|
||||
{/* Certifications Column */}
|
||||
{certifications && certifications.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold font-poppins text-gray-800 dark:text-gray-100 mb-6 text-center md:text-left">
|
||||
Our Certifications
|
||||
</h3>
|
||||
<div className="space-y-6">
|
||||
{certifications.map((cert) => (
|
||||
<div
|
||||
key={cert.id}
|
||||
className="flex items-center bg-gray-50 dark:bg-gray-800 p-4 rounded-lg shadow-sm"
|
||||
>
|
||||
{cert.logoUrl && (
|
||||
<div className="relative h-12 w-16 mr-4 flex-shrink-0">
|
||||
<Image
|
||||
src={cert.logoUrl}
|
||||
alt={`${cert.name} Logo`}
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!cert.logoUrl && (
|
||||
<FaCertificate
|
||||
className="text-2xl mr-4 flex-shrink-0"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
{cert.name}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{cert.issuingBody}
|
||||
</p>
|
||||
{cert.details && (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-300 mt-1">
|
||||
{cert.details}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Partnerships Column */}
|
||||
{partners && partners.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold font-poppins text-gray-800 dark:text-gray-100 mb-6 text-center md:text-left">
|
||||
Key Partners
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-6 items-center">
|
||||
{partners.map((partner) => (
|
||||
<div key={partner.id} className="text-center group">
|
||||
<a
|
||||
href={partner.websiteUrl || "#"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`relative block h-16 md:h-20 filter grayscale group-hover:grayscale-0 transition duration-300 ${
|
||||
!partner.websiteUrl ? "cursor-default" : ""
|
||||
}`}
|
||||
title={partner.name}
|
||||
>
|
||||
<Image
|
||||
src={partner.logoUrl}
|
||||
alt={`${partner.name} Logo`}
|
||||
layout="fill"
|
||||
objectFit="contain"
|
||||
/>
|
||||
</a>
|
||||
{partner.websiteUrl && (
|
||||
<a
|
||||
href={partner.websiteUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-gray-500 dark:text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity duration-300 inline-flex items-center mt-1"
|
||||
>
|
||||
Visit <FaExternalLinkAlt className="ml-1 w-2 h-2" />
|
||||
</a>
|
||||
)}
|
||||
{!partner.websiteUrl && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 mt-1 inline-block">
|
||||
{partner.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{partners.length > 6 && (
|
||||
<p className="text-center text-sm text-gray-500 dark:text-gray-400 mt-4">
|
||||
And more...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 5. Milestones Section */}
|
||||
{milestones && milestones.length > 0 && (
|
||||
<section className="py-16 md:py-20 bg-gray-50 dark:bg-gray-800">
|
||||
<div className="container mx-auto px-6 max-w-3xl">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-center text-gray-900 dark:text-white mb-12">
|
||||
<FaStar
|
||||
className="inline-block mr-3 mb-1"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>{" "}
|
||||
Key Milestones
|
||||
</h2>
|
||||
<div
|
||||
className="relative pl-8 border-l-2 border-gold-500 dark:border-gold-400"
|
||||
style={{ borderColor: COLORS.primary }}
|
||||
>
|
||||
{milestones.map((milestone) => (
|
||||
<div key={milestone.id} className="mb-8 relative">
|
||||
<div
|
||||
className="absolute -left-[1.6rem] top-1 w-6 h-6 bg-gold-500 rounded-full border-4 border-white dark:border-gray-800 dark:bg-gold-400"
|
||||
style={{
|
||||
backgroundColor: COLORS.primary,
|
||||
borderColor:
|
||||
bgColor === "dark"
|
||||
? "#1f2937"
|
||||
: "#f9fafb" /* Adjust based on actual dark/light bg */,
|
||||
}}
|
||||
></div>
|
||||
<p
|
||||
className="text-sm font-semibold text-gold-600 dark:text-gold-400"
|
||||
style={{ color: COLORS.primary }}
|
||||
>
|
||||
{milestone.date}
|
||||
</p>
|
||||
<p className="text-md text-gray-700 dark:text-gray-300">
|
||||
{milestone.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 6. Gallery Section */}
|
||||
{galleryImages && galleryImages.length > 0 && (
|
||||
<section className="py-16 md:py-20 bg-white dark:bg-gray-900">
|
||||
<div className="container mx-auto px-6">
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-center text-gray-900 dark:text-white mb-12">
|
||||
<FaCamera
|
||||
className="inline-block mr-3 mb-1"
|
||||
style={{ color: COLORS.primary }}
|
||||
/>{" "}
|
||||
Moments & Memories
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-6">
|
||||
{galleryImages.map((image) => (
|
||||
<div
|
||||
key={image.id}
|
||||
className="group relative aspect-square overflow-hidden rounded-lg shadow-md cursor-pointer"
|
||||
>
|
||||
{/* Consider adding a Lightbox library here for better image viewing */}
|
||||
<Image
|
||||
src={image.imageUrl}
|
||||
alt={image.alt}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="transform transition-transform duration-300 group-hover:scale-110"
|
||||
/>
|
||||
{/* Overlay for caption */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end p-3 md:p-4">
|
||||
<p className="text-white text-xs md:text-sm font-semibold line-clamp-2">
|
||||
{image.caption || image.alt}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 7. Optional CTA Section */}
|
||||
<CallToActionSection
|
||||
title="Partner with Proven Expertise"
|
||||
subtitle="Let our recognized capabilities help you achieve your technology goals."
|
||||
buttonText="Discuss Your Project"
|
||||
buttonHref="/contact"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to determine background color for milestone border (if needed)
|
||||
// This is a simplified check, adjust based on how you handle theme toggling
|
||||
const bgColor =
|
||||
typeof window !== "undefined" &&
|
||||
document.documentElement.classList.contains("dark")
|
||||
? "dark"
|
||||
: "light";
|
||||
19
app/api/vacancies/[slug]/route.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { demoVacancies } from "@/lib/demo-data/vacancies";
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ slug: string }> }
|
||||
) {
|
||||
const { slug } = await params;
|
||||
// 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);
|
||||
}
|
||||
9
app/api/vacancies/route.ts
Normal file
@ -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);
|
||||
}
|
||||
@ -130,6 +130,21 @@
|
||||
.animation-delay-600 {
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
@keyframes marquee-continuous {
|
||||
0% {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
.animate-marquee-continuous {
|
||||
animation: marquee-continuous linear infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
.group:hover .animate-marquee-continuous {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
}
|
||||
/* Optional scale animation for background */
|
||||
@keyframes floatSlightly {
|
||||
|
||||
@ -20,22 +20,25 @@ const BlogPostCard: React.FC<BlogPostCardProps> = ({
|
||||
author,
|
||||
date,
|
||||
}) => {
|
||||
console.log("BlogPostCard Props:", { imageUrl });
|
||||
|
||||
return (
|
||||
<Link href={`/tech-talk/${slug}`} passHref>
|
||||
<div className="group bg-card border border-border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-all duration-300 ease-in-out flex flex-col h-full transform hover:-translate-y-1">
|
||||
{/* Image Container */}
|
||||
<div className="relative w-full h-48 overflow-hidden">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={title}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
className="transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
{/* Optional: Subtle overlay on hover */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div className="relative w-full aspect-video overflow-hidden">
|
||||
{imageUrl ? (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={title}
|
||||
fill // Use fill instead of layout
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105" // Use object-cover class
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
/>
|
||||
) : (
|
||||
// Optional: Placeholder if no image
|
||||
<div className="w-full h-full bg-muted flex items-center justify-center">
|
||||
<span className="text-muted-foreground text-sm">No Image</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
|
||||
@ -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 (
|
||||
<Button type="submit" variant="primary" size="lg" disabled={pending}>
|
||||
{pending ? "Sending..." : "Send Message"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
// 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<typeof contactSchema>;
|
||||
|
||||
// The main contact form component
|
||||
export default function ContactForm() {
|
||||
// React Hook Form setup
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isValid },
|
||||
reset,
|
||||
} = useForm<ContactFormData>({
|
||||
resolver: zodResolver(contactSchema),
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const initialState: ContactFormState = {
|
||||
message: null,
|
||||
errors: {},
|
||||
success: false,
|
||||
};
|
||||
const [state, dispatch] = useActionState(submitContactForm, initialState);
|
||||
const formRef = useRef<HTMLFormElement>(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 (
|
||||
<form ref={formRef} action={dispatch} className="space-y-6">
|
||||
{/* Name Input */}
|
||||
<form action={formAction} className="space-y-6">
|
||||
{/* Name Field */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="name"
|
||||
@ -43,24 +57,40 @@ export default function ContactForm() {
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
required
|
||||
className="block w-full px-4 py-2 border border-border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
|
||||
type="text"
|
||||
{...register("name")}
|
||||
aria-invalid={errors.name ? "true" : "false"}
|
||||
aria-describedby="name-error"
|
||||
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 dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:placeholder-gray-400 ${
|
||||
errors.name
|
||||
? "border-destructive"
|
||||
: "border-border dark:border-gray-600"
|
||||
}`}
|
||||
/>
|
||||
<div id="name-error" aria-live="polite" aria-atomic="true">
|
||||
{state.errors?.name &&
|
||||
state.errors.name.map((error: string) => (
|
||||
<p className="mt-1 text-sm text-destructive" key={error}>
|
||||
{error}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
{/* Client-side error */}
|
||||
{errors.name && (
|
||||
<p
|
||||
id="name-error"
|
||||
role="alert"
|
||||
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||
>
|
||||
{errors.name.message}
|
||||
</p>
|
||||
)}
|
||||
{/* Server-side error */}
|
||||
{state.errors?.name &&
|
||||
state.errors.name.map((err: string) => (
|
||||
<p
|
||||
key={err}
|
||||
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||
>
|
||||
{err}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Email Input */}
|
||||
{/* Email Field */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
@ -69,24 +99,38 @@ export default function ContactForm() {
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
className="block w-full px-4 py-2 border border-border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
|
||||
type="email"
|
||||
{...register("email")}
|
||||
aria-invalid={errors.email ? "true" : "false"}
|
||||
aria-describedby="email-error"
|
||||
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 dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:placeholder-gray-400 ${
|
||||
errors.email
|
||||
? "border-destructive"
|
||||
: "border-border dark:border-gray-600"
|
||||
}`}
|
||||
/>
|
||||
<div id="email-error" aria-live="polite" aria-atomic="true">
|
||||
{state.errors?.email &&
|
||||
state.errors.email.map((error: string) => (
|
||||
<p className="mt-1 text-sm text-destructive" key={error}>
|
||||
{error}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
{errors.email && (
|
||||
<p
|
||||
id="email-error"
|
||||
role="alert"
|
||||
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||
>
|
||||
{errors.email.message}
|
||||
</p>
|
||||
)}
|
||||
{state.errors?.email &&
|
||||
state.errors.email.map((err: string) => (
|
||||
<p
|
||||
key={err}
|
||||
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||
>
|
||||
{err}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Subject Input */}
|
||||
{/* Subject Field */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="subject"
|
||||
@ -95,24 +139,38 @@ export default function ContactForm() {
|
||||
Subject
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
name="subject"
|
||||
required
|
||||
className="block w-full px-4 py-2 border border-border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
|
||||
type="text"
|
||||
{...register("subject")}
|
||||
aria-invalid={errors.subject ? "true" : "false"}
|
||||
aria-describedby="subject-error"
|
||||
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 dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:placeholder-gray-400 ${
|
||||
errors.subject
|
||||
? "border-destructive"
|
||||
: "border-border dark:border-gray-600"
|
||||
}`}
|
||||
/>
|
||||
<div id="subject-error" aria-live="polite" aria-atomic="true">
|
||||
{state.errors?.subject &&
|
||||
state.errors.subject.map((error: string) => (
|
||||
<p className="mt-1 text-sm text-destructive" key={error}>
|
||||
{error}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
{errors.subject && (
|
||||
<p
|
||||
id="subject-error"
|
||||
role="alert"
|
||||
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||
>
|
||||
{errors.subject.message}
|
||||
</p>
|
||||
)}
|
||||
{state.errors?.subject &&
|
||||
state.errors.subject.map((err: string) => (
|
||||
<p
|
||||
key={err}
|
||||
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||
>
|
||||
{err}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Message Textarea */}
|
||||
{/* Message Field */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="message"
|
||||
@ -122,38 +180,65 @@ export default function ContactForm() {
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
rows={5}
|
||||
required
|
||||
className="block w-full px-4 py-2 border border-border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
|
||||
{...register("message")}
|
||||
aria-invalid={errors.message ? "true" : "false"}
|
||||
aria-describedby="message-error"
|
||||
></textarea>
|
||||
<div id="message-error" aria-live="polite" aria-atomic="true">
|
||||
{state.errors?.message &&
|
||||
state.errors.message.map((error: string) => (
|
||||
<p className="mt-1 text-sm text-destructive" key={error}>
|
||||
{error}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* General Form Message (Success or Error) */}
|
||||
<div id="form-response" aria-live="polite" aria-atomic="true">
|
||||
{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 && (
|
||||
<p
|
||||
className={`text-sm ${
|
||||
state.success ? "text-green-600" : "text-destructive"
|
||||
}`}
|
||||
id="message-error"
|
||||
role="alert"
|
||||
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||
>
|
||||
{state.message}
|
||||
{errors.message.message}
|
||||
</p>
|
||||
)}
|
||||
{state.errors?.message &&
|
||||
state.errors.message.map((err: string) => (
|
||||
<p
|
||||
key={err}
|
||||
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||
>
|
||||
{err}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* General Form Response */}
|
||||
{state.message && (
|
||||
<p
|
||||
className={`text-sm ${
|
||||
state.success
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: "text-destructive dark:text-red-400"
|
||||
}`}
|
||||
>
|
||||
{state.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
{/* isSubmitting from useFormState is now implicitly handled by the form action */}
|
||||
<div className="pt-2">
|
||||
<SubmitButton />
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
// Disable based on client-side validation only before submission starts
|
||||
// The form handles disabling during submission automatically
|
||||
disabled={!isValid}
|
||||
// aria-disabled={pending} // You might need to import useFormStatus for pending state
|
||||
>
|
||||
{/* {pending ? "Sending..." : "Send Message"} // Use useFormStatus for pending state */}
|
||||
Send Message{" "}
|
||||
{/* Simplified button text, pending state handled by browser/React */}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
@ -178,7 +178,7 @@ const Footer = () => {
|
||||
Salesforce Partner
|
||||
</span>
|
||||
<span className="inline-block bg-[var(--dark-secondary)] text-[var(--dark-secondary-foreground)] px-3 py-1 rounded-md text-xs font-medium">
|
||||
BBB-EE Level X
|
||||
BBB-EE Level 1
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,23 +4,25 @@
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
// Use usePathname only if needed for active state logic not shown here
|
||||
// import { usePathname } from "next/navigation";
|
||||
import {
|
||||
FiChevronDown,
|
||||
FiClipboard,
|
||||
FiArrowRight,
|
||||
FiMenu,
|
||||
FiX,
|
||||
FiLogIn, // Import the login icon
|
||||
FiLogIn,
|
||||
FiUsers, // Resource Augmentation
|
||||
FiBriefcase, // Project Management
|
||||
FiCpu, // Product Development
|
||||
FiBox, // OBSE
|
||||
FiFileText, // Vacancies
|
||||
FiUserCheck, // Recruitment Portal
|
||||
} from "react-icons/fi";
|
||||
import ThemeToggle from "./ThemeToggle"; // Assuming ThemeToggle component exists
|
||||
import type { Session } from "@auth/core/types"; // Import Session type for props
|
||||
import ThemeToggle from "./ThemeToggle";
|
||||
import type { Session } from "@auth/core/types";
|
||||
|
||||
const omsLogoUrl = "/oms-logo.svg"; // Ensure this is in your /public folder
|
||||
const omsLogoUrl = "/oms-logo.svg";
|
||||
|
||||
// --- Basic Dropdown Component ---
|
||||
// (Keep the DropdownMenu and DropdownLink component definitions here as before)
|
||||
type DropdownMenuProps = {
|
||||
trigger: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
@ -31,7 +33,6 @@ const DropdownMenu = ({
|
||||
children,
|
||||
menuClasses = "w-48",
|
||||
}: DropdownMenuProps) => (
|
||||
// Using group-focus-within for better keyboard/touch accessibility
|
||||
<div className="relative group">
|
||||
<button className="flex items-center space-x-1 text-sm font-medium focus:outline-none inherit-color group">
|
||||
{trigger}
|
||||
@ -40,8 +41,9 @@ const DropdownMenu = ({
|
||||
<div
|
||||
className={`
|
||||
absolute left-0 mt-2 ${menuClasses} origin-top-left rounded-md shadow-lg z-50
|
||||
bg-card border border-border {/* USE SEMANTIC VARS */}
|
||||
opacity-0 invisible group-hover:opacity-100 group-hover:visible group-focus-within:opacity-100 group-focus-within:visible transition-all duration-200
|
||||
bg-card border border-border
|
||||
opacity-0 invisible group-hover:opacity-100 group-hover:visible group-focus-within:opacity-100 group-focus-within:visible
|
||||
transition-all duration-200
|
||||
`}
|
||||
>
|
||||
<div className="py-1">{children}</div>
|
||||
@ -52,143 +54,137 @@ const DropdownMenu = ({
|
||||
type DropdownLinkProps = {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void; // Added onClick for mobile menu closure
|
||||
onClick?: () => void;
|
||||
};
|
||||
const DropdownLink = ({ href, children, onClick }: DropdownLinkProps) => (
|
||||
<Link
|
||||
href={href}
|
||||
onClick={onClick} // Call onClick if provided
|
||||
className="block w-full text-left px-4 py-2 text-sm text-card-foreground hover:bg-secondary hover:text-primary" // USE SEMANTIC VARS
|
||||
onClick={onClick}
|
||||
className="block w-full text-left px-4 py-2 text-sm text-card-foreground hover:bg-secondary hover:text-primary transition-colors duration-150"
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
// --- End Dropdown Component ---
|
||||
|
||||
// --- Define Props for the Client Component ---
|
||||
type HeaderClientProps = {
|
||||
session: Session | null; // Accept session from the server component
|
||||
handleSignIn: () => void; // Pass sign-in handler
|
||||
handleSignOut: () => void; // Pass sign-out handler
|
||||
session: Session | null;
|
||||
handleSignIn: () => void;
|
||||
handleSignOut: () => void;
|
||||
};
|
||||
|
||||
// --- The Main Client Component ---
|
||||
const HeaderClient = ({
|
||||
session,
|
||||
handleSignIn,
|
||||
handleSignOut,
|
||||
}: HeaderClientProps) => {
|
||||
// --- Client-side state and hooks ---
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
// const pathname = usePathname() || "/"; // Uncomment if needed
|
||||
// const isActive = (path: string) => pathname === path; // Uncomment if needed
|
||||
|
||||
const toggleMenu = () => setIsMenuOpen((open) => !open);
|
||||
const handleMobileLinkClick = () => setIsMenuOpen(false);
|
||||
|
||||
const currentLogo = omsLogoUrl;
|
||||
const megaMenuTriggerClasses = `
|
||||
relative inline-flex items-center text-sm font-medium text-primary-foreground group
|
||||
hover:opacity-90 transition-opacity duration-150 ease-in-out pb-1
|
||||
after:content-[''] after:absolute after:left-0 after:bottom-0 after:h-[2px]
|
||||
after:w-0 after:bg-primary-foreground/80 after:transition-all after:duration-300 after:ease-out
|
||||
group-hover:after:w-full group-focus-within:after:w-full
|
||||
`;
|
||||
|
||||
const megaMenuItemClasses = `
|
||||
flex items-center p-3 -m-3 rounded-lg
|
||||
hover:bg-secondary transition-colors duration-150 ease-in-out group
|
||||
`;
|
||||
const megaMenuIconClasses = `flex-shrink-0 h-6 w-6 text-primary group-hover:text-primary-focus mr-4`;
|
||||
const megaMenuTextWrapperClasses = `text-sm`;
|
||||
const megaMenuTitleClasses = `font-semibold text-card-foreground`;
|
||||
|
||||
return (
|
||||
// Use semantic variables for header background and border
|
||||
<header className="sticky top-0 z-50 shadow-md bg-background border-b border-border">
|
||||
{/* Top Row */}
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center space-x-2"
|
||||
title="OMS Home"
|
||||
>
|
||||
<Image
|
||||
src={currentLogo}
|
||||
src={omsLogoUrl}
|
||||
alt="OMS Logo"
|
||||
width={40}
|
||||
height={40}
|
||||
priority
|
||||
/>
|
||||
{/* Use semantic variable for text color */}
|
||||
<span className="text-xl font-bold text-foreground hidden sm:inline">
|
||||
Owethu Managed Services
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center space-x-6 lg:space-x-8">
|
||||
{/* Use semantic variables for text, hover uses primary */}
|
||||
<Link
|
||||
href="/"
|
||||
className={`text-sm font-medium text-foreground/80 hover:text-primary transition`}
|
||||
className="text-sm font-medium text-foreground/80 hover:text-primary transition"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<Link
|
||||
href="/tech-talk"
|
||||
className={`text-sm font-medium text-foreground/80 hover:text-primary transition`}
|
||||
className="text-sm font-medium text-foreground/80 hover:text-primary transition"
|
||||
>
|
||||
Tech Talk
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
className={`text-sm font-medium text-foreground/80 hover:text-primary transition`}
|
||||
className="text-sm font-medium text-foreground/80 hover:text-primary transition"
|
||||
>
|
||||
About Us
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact"
|
||||
className={`text-sm font-medium text-foreground/80 hover:text-primary transition`}
|
||||
className="text-sm font-medium text-foreground/80 hover:text-primary transition"
|
||||
>
|
||||
Contact Us
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
{/* Desktop Utilities */}
|
||||
<div className="hidden md:flex items-center space-x-4">
|
||||
<ThemeToggle />
|
||||
|
||||
{/* Request Demo Button */}
|
||||
<Link
|
||||
href="/request-demo"
|
||||
className="flex items-center text-sm font-medium bg-primary text-primary-foreground px-3 py-1.5 rounded-lg hover:bg-opacity-90 transition-colors"
|
||||
title="Request a Demo"
|
||||
>
|
||||
<FiClipboard className="w-4 h-4 mr-1.5" />
|
||||
Request Demo
|
||||
Request OBSE Demo
|
||||
</Link>
|
||||
|
||||
{/* --- Auth Section --- */}
|
||||
{session?.user ? (
|
||||
// Logged In: User Dropdown
|
||||
<DropdownMenu
|
||||
menuClasses="w-40 right-0 left-auto" // Align dropdown to the right
|
||||
menuClasses="w-40 right-0 left-auto"
|
||||
trigger={
|
||||
<div className="flex items-center space-x-2 cursor-pointer hover:opacity-80 transition">
|
||||
<Image
|
||||
src={session.user.image || "/default-avatar.png"} // Provide a fallback avatar
|
||||
src={session.user.image || "/default-avatar.png"}
|
||||
alt={session.user.name || "User"}
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-full ring-1 ring-primary ring-offset-2 ring-offset-background" // Added offset
|
||||
className="rounded-full ring-1 ring-primary ring-offset-2 ring-offset-background"
|
||||
/>
|
||||
{/* Optionally hide name on smaller desktop screens */}
|
||||
<span className="text-sm font-medium text-foreground hidden lg:inline">
|
||||
{session.user.name?.split(" ")[0]}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/* Sign Out inside the dropdown */}
|
||||
<button
|
||||
onClick={() => handleSignOut()}
|
||||
className="block w-full text-left px-4 py-2 text-sm text-destructive hover:bg-secondary hover:text-destructive transition" // Destructive color
|
||||
onClick={handleSignOut}
|
||||
className="block w-full text-left px-4 py-2 text-sm text-destructive hover:bg-secondary hover:text-destructive transition"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
// Logged Out: Icon Button to Sign In
|
||||
<button
|
||||
onClick={() => handleSignIn()}
|
||||
onClick={handleSignIn}
|
||||
className="p-2 rounded-full text-foreground/70 hover:text-primary hover:bg-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background transition-colors"
|
||||
title="Sign In"
|
||||
aria-label="Sign In"
|
||||
@ -196,15 +192,13 @@ const HeaderClient = ({
|
||||
<FiLogIn className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
{/* --- End Auth Section --- */}
|
||||
</div>
|
||||
|
||||
{/* Mobile Buttons */}
|
||||
<div className="md:hidden flex items-center">
|
||||
<ThemeToggle /> {/* Theme toggle remains */}
|
||||
<ThemeToggle />
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
className="text-foreground/60 hover:text-primary focus:outline-none ml-3" // Use semantic muted color, hover primary
|
||||
className="text-foreground/60 hover:text-primary focus:outline-none ml-3"
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded={isMenuOpen}
|
||||
aria-controls="mobile-menu"
|
||||
@ -219,43 +213,144 @@ const HeaderClient = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Secondary Row (Desktop Only) */}
|
||||
<div className="bg-primary">
|
||||
{/* Secondary Row w/ Mega Menus */}
|
||||
<div className="bg-primary relative">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="hidden md:flex justify-between items-center h-12">
|
||||
<nav className="flex items-center space-x-6 lg:space-x-8 text-primary-foreground">
|
||||
<div className="hover:text-opacity-80 transition-opacity">
|
||||
<DropdownMenu trigger={<span>Services</span>}>
|
||||
<DropdownLink href="/services/resource-augmentation">
|
||||
Resource Augmentation
|
||||
</DropdownLink>
|
||||
<DropdownLink href="/services/project-management">
|
||||
Project Management
|
||||
</DropdownLink>
|
||||
<DropdownLink href="/services/product-development">
|
||||
Product Development
|
||||
</DropdownLink>
|
||||
</DropdownMenu>
|
||||
<nav className="flex items-center space-x-8 lg:space-x-10">
|
||||
{/* Services */}
|
||||
<div className="group">
|
||||
<button className={megaMenuTriggerClasses}>
|
||||
Services
|
||||
<FiChevronDown className="w-4 h-4 ml-1.5 opacity-70 group-hover:rotate-180 transition-transform duration-200" />
|
||||
</button>
|
||||
<div
|
||||
className={`
|
||||
absolute left-0 top-full w-full shadow-lg z-40
|
||||
bg-card border-x border-b border-border rounded-b-lg
|
||||
opacity-0 invisible translate-y-[-10px]
|
||||
group-hover:opacity-100 group-hover:visible group-hover:translate-y-0
|
||||
transition-all duration-300 ease-out transform
|
||||
`}
|
||||
>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-5">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-x-8 gap-y-6">
|
||||
<Link
|
||||
href="/services/resource-augmentation"
|
||||
className={megaMenuItemClasses}
|
||||
>
|
||||
<FiUsers className={megaMenuIconClasses} />
|
||||
<div className={megaMenuTextWrapperClasses}>
|
||||
<p className={megaMenuTitleClasses}>
|
||||
Resource Augmentation
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/services/project-management"
|
||||
className={megaMenuItemClasses}
|
||||
>
|
||||
<FiBriefcase className={megaMenuIconClasses} />
|
||||
<div className={megaMenuTextWrapperClasses}>
|
||||
<p className={megaMenuTitleClasses}>
|
||||
Project Management
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/services/product-development"
|
||||
className={megaMenuItemClasses}
|
||||
>
|
||||
<FiCpu className={megaMenuIconClasses} />
|
||||
<div className={megaMenuTextWrapperClasses}>
|
||||
<p className={megaMenuTitleClasses}>
|
||||
Product Development
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-6 pt-4 border-t border-border flex justify-end">
|
||||
<Link
|
||||
href="/services"
|
||||
className="flex items-center text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
Explore All Services
|
||||
<FiArrowRight className="w-4 h-4 ml-1" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hover:text-opacity-80 transition-opacity">
|
||||
<DropdownMenu trigger={<span>Products</span>}>
|
||||
<DropdownLink href="/products/obse">OBSE</DropdownLink>
|
||||
</DropdownMenu>
|
||||
{/* Products */}
|
||||
<div className="group">
|
||||
<button className={megaMenuTriggerClasses}>
|
||||
Products
|
||||
<FiChevronDown className="w-4 h-4 ml-1.5 opacity-70 group-hover:rotate-180 transition-transform duration-200" />
|
||||
</button>
|
||||
<div
|
||||
className={`
|
||||
absolute left-0 top-full w-full shadow-lg z-40
|
||||
bg-card border-x border-b border-border rounded-b-lg
|
||||
opacity-0 invisible translate-y-[-10px]
|
||||
group-hover:opacity-100 group-hover:visible group-hover:translate-y-0
|
||||
transition-all duration-300 ease-out transform
|
||||
`}
|
||||
>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-5">
|
||||
<div className="max-w-md">
|
||||
<Link href="/obse" className={megaMenuItemClasses}>
|
||||
<FiBox className={megaMenuIconClasses} />
|
||||
<div className={megaMenuTextWrapperClasses}>
|
||||
<p className={megaMenuTitleClasses}>OBSE Platform</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hover:text-opacity-80 transition-opacity">
|
||||
<DropdownMenu trigger={<span>Join Our Team</span>}>
|
||||
<DropdownLink href="/join-us/vacancies">
|
||||
Vacancies
|
||||
</DropdownLink>
|
||||
<DropdownLink href="/join-us/portal">
|
||||
Recruitment Portal
|
||||
</DropdownLink>
|
||||
</DropdownMenu>
|
||||
{/* Join Our Team */}
|
||||
<div className="group">
|
||||
<button className={megaMenuTriggerClasses}>
|
||||
Join Our Team
|
||||
<FiChevronDown className="w-4 h-4 ml-1.5 opacity-70 group-hover:rotate-180 transition-transform duration-200" />
|
||||
</button>
|
||||
<div
|
||||
className={`
|
||||
absolute left-0 top-full w-full shadow-lg z-40
|
||||
bg-card border-x border-b border-border rounded-b-lg
|
||||
opacity-0 invisible translate-y-[-10px]
|
||||
group-hover:opacity-100 group-hover:visible group-hover:translate-y-0
|
||||
transition-all duration-300 ease-out transform
|
||||
`}
|
||||
>
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-5">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 max-w-xl">
|
||||
<Link href="/vacancies" className={megaMenuItemClasses}>
|
||||
<FiFileText className={megaMenuIconClasses} />
|
||||
<div className={megaMenuTextWrapperClasses}>
|
||||
<p className={megaMenuTitleClasses}>
|
||||
Current Vacancies
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href="/portal" className={megaMenuItemClasses}>
|
||||
<FiUserCheck className={megaMenuIconClasses} />
|
||||
<div className={megaMenuTextWrapperClasses}>
|
||||
<p className={megaMenuTitleClasses}>
|
||||
Recruitment Portal
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* ← Here’s the Explore Our Offerings link, back in its original spot */}
|
||||
<Link
|
||||
href="/services"
|
||||
className="flex items-center text-sm font-medium text-primary-foreground hover:text-opacity-80 transition-opacity group"
|
||||
className="flex items-center text-sm font-medium text-primary-foreground hover:opacity-80 transition-opacity group"
|
||||
>
|
||||
Explore Our Offerings
|
||||
<FiArrowRight className="w-4 h-4 ml-1.5 transition-transform duration-200 group-hover:translate-x-1" />
|
||||
@ -264,21 +359,16 @@ const HeaderClient = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Panel */}
|
||||
{/* Mobile Menu */}
|
||||
<div
|
||||
id="mobile-menu"
|
||||
// Use semantic variables for background and border
|
||||
className={`md:hidden absolute top-full left-0 w-full shadow-lg transition-all duration-300 ease-in-out overflow-hidden bg-card border-t border-border ${
|
||||
isMenuOpen
|
||||
? "max-h-[calc(100vh-4rem)] py-4 overflow-y-auto"
|
||||
: "max-h-0 py-0" // Animate height, allow scroll
|
||||
: "max-h-0 py-0"
|
||||
}`}
|
||||
>
|
||||
{/* Use semantic variable for text color */}
|
||||
<nav className="container mx-auto px-4 sm:px-6 lg:px-8 flex flex-col space-y-1 text-foreground">
|
||||
{" "}
|
||||
{/* Reduced space-y */}
|
||||
{/* Mobile Links */}
|
||||
<DropdownLink href="/" onClick={handleMobileLinkClick}>
|
||||
Home
|
||||
</DropdownLink>
|
||||
@ -309,41 +399,33 @@ const HeaderClient = ({
|
||||
<span className="pt-3 pb-1 text-xs uppercase text-muted-foreground">
|
||||
Products
|
||||
</span>
|
||||
<DropdownLink href="/products/obse" onClick={handleMobileLinkClick}>
|
||||
<DropdownLink href="/obse" onClick={handleMobileLinkClick}>
|
||||
OBSE
|
||||
</DropdownLink>
|
||||
<span className="pt-3 pb-1 text-xs uppercase text-muted-foreground">
|
||||
Join Us
|
||||
</span>
|
||||
<DropdownLink
|
||||
href="/join-us/vacancies"
|
||||
onClick={handleMobileLinkClick}
|
||||
>
|
||||
<DropdownLink href="/vacancies" onClick={handleMobileLinkClick}>
|
||||
Vacancies
|
||||
</DropdownLink>
|
||||
<DropdownLink href="/join-us/portal" onClick={handleMobileLinkClick}>
|
||||
<DropdownLink href="/portal" onClick={handleMobileLinkClick}>
|
||||
Recruitment Portal
|
||||
</DropdownLink>
|
||||
<DropdownLink href="/contact" onClick={handleMobileLinkClick}>
|
||||
Contact Us
|
||||
</DropdownLink>
|
||||
{/* Demo Button */}
|
||||
<div className="pt-4">
|
||||
<Link
|
||||
href="/request-demo"
|
||||
onClick={handleMobileLinkClick}
|
||||
// Use semantic variables for button style
|
||||
className="flex w-full justify-center items-center text-sm font-medium bg-primary text-primary-foreground px-4 py-2 rounded-lg hover:bg-opacity-90 transition-colors"
|
||||
title="Request a Demo"
|
||||
>
|
||||
<FiClipboard className="w-4 h-4 mr-1.5" />
|
||||
Request Demo
|
||||
<FiClipboard className="w-4 h-4 mr-1.5" /> Request OBSE Demo
|
||||
</Link>
|
||||
</div>
|
||||
{/* Auth Buttons in Mobile Menu */}
|
||||
<div className="pt-4 border-t border-border mt-4">
|
||||
{session?.user ? (
|
||||
// Sign Out Button (Text)
|
||||
<button
|
||||
onClick={() => {
|
||||
handleSignOut();
|
||||
@ -354,7 +436,6 @@ const HeaderClient = ({
|
||||
Sign Out
|
||||
</button>
|
||||
) : (
|
||||
// Sign In Button (Text or Icon+Text)
|
||||
<button
|
||||
onClick={() => {
|
||||
handleSignIn();
|
||||
@ -362,8 +443,7 @@ const HeaderClient = ({
|
||||
}}
|
||||
className="flex items-center w-full text-left px-4 py-2 text-sm font-medium text-foreground hover:bg-secondary hover:text-primary transition"
|
||||
>
|
||||
<FiLogIn className="w-4 h-4 mr-2" /> {/* Added icon here too */}
|
||||
Sign In
|
||||
<FiLogIn className="w-4 h-4 mr-2" /> Sign In
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -164,7 +164,7 @@ const Header = () => {
|
||||
title="Request a Demo"
|
||||
>
|
||||
<FiClipboard className="w-4 h-4 mr-1.5" />
|
||||
Request Demo
|
||||
Request OBSE Demo
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
374
components/VacancyApplicationForm.tsx
Normal file
@ -0,0 +1,374 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback, ChangeEvent, DragEvent } from "react";
|
||||
import { FaUpload, FaFileAlt, FaTimes } from "react-icons/fa";
|
||||
import { useForm, SubmitHandler } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import Button from "./ui/Button";
|
||||
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const ACCEPTED_FILE_TYPES = [
|
||||
"application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"text/plain",
|
||||
];
|
||||
|
||||
const applicationSchema = z.object({
|
||||
name: z.string().min(2, "Full name must be at least 2 characters"),
|
||||
email: z.string().email("Invalid email address"),
|
||||
phone: z.string().optional(),
|
||||
linkedin: z.string().url("Invalid URL").optional().or(z.literal("")),
|
||||
coverLetter: z.string().optional(),
|
||||
resume: z
|
||||
.custom<File | null>((val) => val instanceof File, "Resume/CV is required")
|
||||
.refine(
|
||||
(file) => file && file.size <= MAX_FILE_SIZE,
|
||||
`Max file size is 5MB.`
|
||||
)
|
||||
.refine(
|
||||
(file) => file && ACCEPTED_FILE_TYPES.includes(file.type),
|
||||
"Unsupported file format. Please upload PDF, DOC, DOCX, or TXT."
|
||||
),
|
||||
});
|
||||
|
||||
type ApplicationFormData = z.infer<typeof applicationSchema>;
|
||||
|
||||
interface VacancyApplicationFormProps {
|
||||
vacancyId: string;
|
||||
vacancyTitle: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function VacancyApplicationForm({
|
||||
vacancyId,
|
||||
vacancyTitle,
|
||||
onClose,
|
||||
}: VacancyApplicationFormProps) {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors, isSubmitting },
|
||||
reset,
|
||||
} = useForm<ApplicationFormData>({
|
||||
resolver: zodResolver(applicationSchema),
|
||||
mode: "onChange",
|
||||
defaultValues: {
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
linkedin: "",
|
||||
coverLetter: "",
|
||||
resume: null,
|
||||
},
|
||||
});
|
||||
|
||||
const resumeFile = watch("resume");
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [submitStatus, setSubmitStatus] = useState<
|
||||
"idle" | "success" | "error"
|
||||
>("idle");
|
||||
|
||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
setValue("resume", e.target.files[0], { shouldValidate: true });
|
||||
e.target.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnter = useCallback((e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
const handleDragOver = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isDragging) setIsDragging(true);
|
||||
},
|
||||
[isDragging]
|
||||
);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
setValue("resume", e.dataTransfer.files[0], { shouldValidate: true });
|
||||
e.dataTransfer.clearData();
|
||||
}
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const removeFile = () => {
|
||||
setValue("resume", null, { shouldValidate: true });
|
||||
};
|
||||
|
||||
const onSubmit: SubmitHandler<ApplicationFormData> = async (data) => {
|
||||
setSubmitStatus("idle");
|
||||
const formData = new FormData();
|
||||
formData.append("vacancyId", vacancyId);
|
||||
formData.append("vacancyTitle", vacancyTitle);
|
||||
formData.append("name", data.name);
|
||||
formData.append("email", data.email);
|
||||
if (data.phone) formData.append("phone", data.phone);
|
||||
if (data.linkedin) formData.append("linkedin", data.linkedin);
|
||||
if (data.coverLetter) formData.append("coverLetter", data.coverLetter);
|
||||
if (data.resume) formData.append("resume", data.resume, data.resume.name);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/apply", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (!response.ok)
|
||||
throw new Error(`Submission failed: ${response.statusText}`);
|
||||
setSubmitStatus("success");
|
||||
reset();
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
setSubmitStatus("idle");
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setSubmitStatus("error");
|
||||
}
|
||||
};
|
||||
|
||||
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";
|
||||
const errorStyle = "border-red-500 focus:border-red-500 focus:ring-red-500";
|
||||
const errorMessageStyle = "text-red-600 text-xs mt-1";
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 font-poppins">
|
||||
{/* Row 1: Name & Email */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Name Field */}
|
||||
<div>
|
||||
<label htmlFor="name" className="text-gray-700 dark:text-gray-200">
|
||||
Full Name *
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
{...register("name")}
|
||||
className={`${inputBaseStyle} ${errors.name ? errorStyle : ""}`}
|
||||
aria-invalid={errors.name ? "true" : "false"}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p role="alert" className={errorMessageStyle}>
|
||||
{errors.name.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Email Field */}
|
||||
<div>
|
||||
<label htmlFor="email" className="text-gray-700 dark:text-gray-200">
|
||||
Email Address *
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
{...register("email")}
|
||||
className={`${inputBaseStyle} ${errors.email ? errorStyle : ""}`}
|
||||
aria-invalid={errors.email ? "true" : "false"}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p role="alert" className={errorMessageStyle}>
|
||||
{errors.email.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Phone & LinkedIn */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Phone Field */}
|
||||
<div>
|
||||
<label htmlFor="phone" className="text-gray-700 dark:text-gray-200">
|
||||
Phone Number
|
||||
</label>
|
||||
<input
|
||||
id="phone"
|
||||
type="tel"
|
||||
{...register("phone")}
|
||||
className={`${inputBaseStyle} ${errors.phone ? errorStyle : ""}`}
|
||||
aria-invalid={errors.phone ? "true" : "false"}
|
||||
/>
|
||||
{errors.phone && (
|
||||
<p role="alert" className={errorMessageStyle}>
|
||||
{errors.phone.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* LinkedIn Field */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="linkedin"
|
||||
className="text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
LinkedIn Profile URL
|
||||
</label>
|
||||
<input
|
||||
id="linkedin"
|
||||
type="url"
|
||||
{...register("linkedin")}
|
||||
className={`${inputBaseStyle} ${errors.linkedin ? errorStyle : ""}`}
|
||||
placeholder="https://linkedin.com/in/yourprofile"
|
||||
aria-invalid={errors.linkedin ? "true" : "false"}
|
||||
/>
|
||||
{errors.linkedin && (
|
||||
<p role="alert" className={errorMessageStyle}>
|
||||
{errors.linkedin.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resume/CV Upload */}
|
||||
<div>
|
||||
<label className="text-gray-700 dark:text-gray-200 block mb-2">
|
||||
Resume/CV *
|
||||
</label>
|
||||
<input
|
||||
id="resume-upload-hidden"
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
accept=".pdf,.doc,.docx,.txt"
|
||||
className="hidden"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
className={`relative flex flex-col items-center justify-center w-full p-6 transition-colors duration-300 ease-in-out border-2 border-dashed rounded-lg cursor-pointer hover:border-gold-500/80 ${
|
||||
isDragging
|
||||
? "border-gold-500 bg-gold-50"
|
||||
: errors.resume
|
||||
? "border-red-500"
|
||||
: "border-gray-300 hover:bg-gray-50"
|
||||
}`}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
onClick={() =>
|
||||
document.getElementById("resume-upload-hidden")?.click()
|
||||
}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ")
|
||||
document.getElementById("resume-upload-hidden")?.click();
|
||||
}}
|
||||
aria-label="Upload Resume/CV"
|
||||
>
|
||||
<FaUpload
|
||||
className={`w-8 h-8 mb-3 ${
|
||||
isDragging ? "text-gold-600" : "text-gray-400"
|
||||
}`}
|
||||
style={{ color: isDragging ? "#c8a93d" : undefined }}
|
||||
/>
|
||||
<p
|
||||
className={`text-sm font-semibold text-center ${
|
||||
isDragging ? "text-gold-700" : "text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{isDragging ? "Drop file here" : "Drag & drop your file here"}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
or click to browse
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
(PDF, DOC, DOCX, TXT - Max 5MB)
|
||||
</p>
|
||||
</div>
|
||||
{errors.resume && (
|
||||
<p role="alert" className={errorMessageStyle}>
|
||||
{(errors.resume as unknown as { message: string }).message}
|
||||
</p>
|
||||
)}
|
||||
{resumeFile && (
|
||||
<div className="mt-3 flex items-center justify-between px-3 py-2 text-sm text-gray-700 bg-gray-100 border border-gray-200 rounded-md">
|
||||
<div className="flex items-center gap-2">
|
||||
<FaFileAlt className="w-4 h-4 text-gray-500" />
|
||||
<span className="font-medium truncate max-w-[200px] sm:max-w-xs">
|
||||
{resumeFile.name}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
({(resumeFile.size / 1024).toFixed(1)} KB)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={removeFile}
|
||||
className="text-red-500 hover:text-red-700 focus:outline-none"
|
||||
aria-label="Remove file"
|
||||
>
|
||||
<FaTimes className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cover Letter Field */}
|
||||
<div>
|
||||
<label
|
||||
htmlFor="coverLetter"
|
||||
className="text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
Cover Letter (Optional)
|
||||
</label>
|
||||
<textarea
|
||||
id="coverLetter"
|
||||
rows={4}
|
||||
{...register("coverLetter")} // Register with RHF
|
||||
className={`${inputBaseStyle} resize-vertical ${
|
||||
errors.coverLetter ? errorStyle : ""
|
||||
}`}
|
||||
aria-invalid={errors.coverLetter ? "true" : "false"}
|
||||
></textarea>
|
||||
{errors.coverLetter && (
|
||||
<p role="alert" className={errorMessageStyle}>
|
||||
{errors.coverLetter.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit Button & Status */}
|
||||
<div className="flex flex-col items-center space-y-3 pt-4">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || submitStatus === "success"} // Disable while submitting or on success
|
||||
>
|
||||
{isSubmitting ? "Submitting..." : "Submit Application"}
|
||||
</Button>
|
||||
|
||||
{submitStatus === "success" && (
|
||||
<p className="text-sm text-green-600">
|
||||
Application submitted successfully! Closing form...
|
||||
</p>
|
||||
)}
|
||||
{submitStatus === "error" && (
|
||||
<p className="text-sm text-red-600">
|
||||
Submission failed. Please check your details and try again.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@ -20,7 +20,7 @@ const Button: React.FC<ButtonProps> = ({
|
||||
}) => {
|
||||
// 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 = {
|
||||
|
||||
19
components/ui/CustomButton.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
const CustomButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={`inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 ${className}`}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
CustomButton.displayName = "CustomButton";
|
||||
|
||||
export { CustomButton };
|
||||
19
components/ui/CustomInput.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
type CustomInputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
const CustomInput = React.forwardRef<HTMLInputElement, CustomInputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={`flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
CustomInput.displayName = "CustomInput";
|
||||
|
||||
export { CustomInput };
|
||||
18
components/ui/CustomLabel.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
type CustomLabelProps = React.LabelHTMLAttributes<HTMLLabelElement>;
|
||||
|
||||
const CustomLabel = React.forwardRef<HTMLLabelElement, CustomLabelProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<label
|
||||
ref={ref}
|
||||
className={`text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
CustomLabel.displayName = "CustomLabel";
|
||||
|
||||
export { CustomLabel };
|
||||
19
components/ui/CustomTextarea.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
type CustomTextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||
|
||||
const CustomTextarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
CustomTextareaProps
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={`flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
CustomTextarea.displayName = "CustomTextarea";
|
||||
|
||||
export { CustomTextarea };
|
||||
126
components/ui/Modal.tsx
Normal file
@ -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<ModalProps> = ({
|
||||
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
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-60 backdrop-blur-sm p-4 transition-opacity duration-300 ease-in-out"
|
||||
onClick={onClose} // Close when clicking the backdrop
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={title ? "modal-title" : undefined}
|
||||
>
|
||||
{/* Modal Panel */}
|
||||
<div
|
||||
className={`relative w-full ${sizeClasses[size]} bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 ease-in-out transform scale-95 opacity-0 animate-modal-appear`}
|
||||
onClick={(e) => e.stopPropagation()} // Prevent clicks inside the modal from closing it
|
||||
>
|
||||
{/* Modal Header (Optional) */}
|
||||
{title && (
|
||||
<div className="flex items-center justify-between p-4 sm:p-5 border-b border-gray-200 dark:border-gray-700">
|
||||
{title && (
|
||||
<h2
|
||||
id="modal-title"
|
||||
className="text-lg font-semibold text-gray-900 dark:text-white font-poppins"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className="p-5 sm:p-6 space-y-4 max-h-[80vh] overflow-y-auto">
|
||||
{" "}
|
||||
{/* Added max-height and scroll */}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add animation keyframes to your global CSS (e.g., src/app/globals.css) */}
|
||||
<style jsx global>{`
|
||||
@keyframes modal-appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.animate-modal-appear {
|
||||
animation: modal-appear 0.3s ease-out forwards;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
9
constants/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const COLORS = {
|
||||
primary: "#e1c44a",
|
||||
secondary: "#f50057",
|
||||
accent: "#ff5722",
|
||||
success: "#4caf50",
|
||||
warning: "#ff9800",
|
||||
error: "#f44336",
|
||||
info: "#2196f3",
|
||||
};
|
||||
163
lib/demo-data/vacancies.ts
Normal file
@ -0,0 +1,163 @@
|
||||
import { Vacancy } from "@/types";
|
||||
|
||||
export const demoVacancies: Vacancy[] = [
|
||||
{
|
||||
id: "1",
|
||||
title: "Senior Frontend Engineer",
|
||||
slug: "senior-frontend-engineer",
|
||||
description:
|
||||
"We are looking for a talented Senior Frontend Engineer to join our growing team. You will be responsible for building and maintaining our user-facing web applications, ensuring high performance and responsiveness.",
|
||||
department: "Engineering",
|
||||
location: {
|
||||
city: "Cape Town",
|
||||
country: "South Africa",
|
||||
remote: false,
|
||||
},
|
||||
employmentType: "Full-time",
|
||||
experienceLevel: "Senior-level",
|
||||
salary: {
|
||||
min: 70000,
|
||||
max: 90000,
|
||||
currency: "ZAR",
|
||||
period: "Annual",
|
||||
},
|
||||
responsibilities: [
|
||||
"Develop new user-facing features using React and TypeScript.",
|
||||
"Build reusable code and libraries for future use.",
|
||||
"Ensure the technical feasibility of UI/UX designs.",
|
||||
"Optimize application for maximum speed and scalability.",
|
||||
"Collaborate with other team members and stakeholders.",
|
||||
"Mentor junior engineers.",
|
||||
],
|
||||
requiredQualifications: [
|
||||
"5+ years of experience in frontend development.",
|
||||
"Proficient understanding of web markup, including HTML5, CSS3.",
|
||||
"Strong proficiency in JavaScript, including DOM manipulation and the JavaScript object model.",
|
||||
"Thorough understanding of React.js and its core principles.",
|
||||
"Experience with popular React.js workflows (such as Flux or Redux).",
|
||||
"Familiarity with newer specifications of EcmaScript.",
|
||||
"Experience with data structure libraries (e.g., Immutable.js).",
|
||||
"Knowledge of modern authorization mechanisms, such as JSON Web Token.",
|
||||
"Familiarity with modern front-end build pipelines and tools.",
|
||||
"Experience with common front-end development tools such as Babel, Webpack, NPM, etc.",
|
||||
"Ability to understand business requirements and translate them into technical requirements.",
|
||||
"A knack for benchmarking and optimization.",
|
||||
"Familiarity with code versioning tools, such as Git.",
|
||||
],
|
||||
preferredQualifications: [
|
||||
"Experience with Next.js.",
|
||||
"Experience with TypeScript.",
|
||||
"Experience with testing frameworks like Jest or React Testing Library.",
|
||||
"Familiarity with GraphQL.",
|
||||
"Experience working in an Agile/Scrum development process.",
|
||||
],
|
||||
benefits: [
|
||||
"Competitive salary and benefits package.",
|
||||
"Opportunity to work on challenging projects.",
|
||||
"Collaborative and supportive work environment.",
|
||||
"Generous vacation policy.",
|
||||
"Professional development opportunities.",
|
||||
],
|
||||
applicationDeadline: "2025-06-15T23:59:59Z",
|
||||
postedDate: "2025-04-20T09:00:00Z",
|
||||
contactPerson: {
|
||||
name: "Jane Doe",
|
||||
email: "jane.doe@example.com",
|
||||
},
|
||||
status: "Open",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Product Marketing Manager",
|
||||
slug: "product-marketing-manager",
|
||||
description:
|
||||
"Seeking an experienced Product Marketing Manager to lead the go-to-market strategy for our innovative products. You will craft compelling messaging and positioning, drive demand, and enable sales.",
|
||||
department: "Marketing",
|
||||
location: {
|
||||
city: "Pretoria",
|
||||
country: "South Africa",
|
||||
remote: true,
|
||||
},
|
||||
employmentType: "Full-time",
|
||||
experienceLevel: "Mid-level",
|
||||
salary: {
|
||||
min: 60000,
|
||||
max: 80000,
|
||||
currency: "ZAR",
|
||||
period: "Annual",
|
||||
},
|
||||
responsibilities: [
|
||||
"Develop product positioning and messaging that differentiates our products in the market.",
|
||||
"Sales enablement – communicate the value proposition of the products to the sales team and develop the sales tools that support the selling process.",
|
||||
"Product launch – plan the launch of new products and releases and manage the cross-functional implementation of the plan.",
|
||||
"Market intelligence – be the expert on buyers, how they buy and their buying criteria; be the expert on competition.",
|
||||
"Demand generation – develop the strategy and manage the marketing programs that drive demand for your products.",
|
||||
],
|
||||
requiredQualifications: [
|
||||
"3+ years of product marketing experience with B2B SaaS products.",
|
||||
"Proven track record of successfully launching and marketing products.",
|
||||
"Excellent written and verbal communication skills.",
|
||||
"Strong analytical and project management skills.",
|
||||
"Bachelor’s degree in Marketing, Business, or related field.",
|
||||
],
|
||||
preferredQualifications: [
|
||||
"Experience in the tech industry.",
|
||||
"Familiarity with marketing automation tools (e.g., HubSpot, Marketo).",
|
||||
"MBA is a plus.",
|
||||
],
|
||||
benefits: [
|
||||
"Competitive compensation.",
|
||||
"Health, dental, and vision insurance.",
|
||||
"Flexible working hours and remote work options.",
|
||||
"Stock options.",
|
||||
],
|
||||
postedDate: "2025-04-25T10:00:00Z",
|
||||
status: "Open",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Backend Developer (Node.js)",
|
||||
slug: "backend-developer-nodejs",
|
||||
description:
|
||||
"Join our backend team to design, develop, and maintain server-side logic, databases, and APIs for our platform. Focus on scalability, performance, and reliability.",
|
||||
department: "Engineering",
|
||||
location: {
|
||||
city: "Johannesburg",
|
||||
country: "South Africa",
|
||||
remote: false,
|
||||
},
|
||||
employmentType: "Full-time",
|
||||
experienceLevel: "Mid-level",
|
||||
responsibilities: [
|
||||
"Design and implement low-latency, high-availability, and performant applications.",
|
||||
"Write reusable, testable, and efficient code.",
|
||||
"Integration of user-facing elements developed by front-end developers with server side logic.",
|
||||
"Implementation of security and data protection.",
|
||||
"Integration of data storage solutions (may include databases, key-value stores, blob stores, etc.).",
|
||||
],
|
||||
requiredQualifications: [
|
||||
"3+ years of experience as a Backend Developer.",
|
||||
"Strong proficiency with Node.js and JavaScript/TypeScript.",
|
||||
"Experience with frameworks such as Express, Koa, or NestJS.",
|
||||
"Understanding the nature of asynchronous programming and its quirks and workarounds.",
|
||||
"Experience with RESTful APIs design and implementation.",
|
||||
"Knowledge of database technologies (SQL and NoSQL).",
|
||||
"Understanding fundamental design principles behind a scalable application.",
|
||||
"Proficient understanding of code versioning tools, such as Git.",
|
||||
],
|
||||
preferredQualifications: [
|
||||
"Experience with microservices architecture.",
|
||||
"Familiarity with Docker and Kubernetes.",
|
||||
"Experience with cloud platforms like AWS, Azure, or GCP.",
|
||||
"Knowledge of message queue systems (e.g., RabbitMQ, Kafka).",
|
||||
],
|
||||
benefits: [
|
||||
"Opportunity to work with modern technologies.",
|
||||
"Dynamic and international team.",
|
||||
"Regular team events.",
|
||||
"Public transport ticket.",
|
||||
],
|
||||
postedDate: "2025-04-27T08:30:00Z",
|
||||
status: "Open",
|
||||
},
|
||||
];
|
||||
5
lib/directus.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { createDirectus, rest } from "@directus/sdk";
|
||||
|
||||
export const directus = createDirectus(
|
||||
String(process.env.DIRECTUS_API_ENDPOINT!)
|
||||
).with(rest());
|
||||
13
lib/helpers.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const formatDate = (dateString?: string): string => {
|
||||
if (!dateString) return "N/A";
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString("en-ZA", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Invalid date format:", dateString, e);
|
||||
return "Invalid Date";
|
||||
}
|
||||
};
|
||||
8
lib/query/home.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { readItems } from "@directus/sdk";
|
||||
import { directus } from "../directus";
|
||||
import { HeroSection } from "@/types";
|
||||
|
||||
export async function getHome() {
|
||||
// Assuming '1' is the primary key for the singleton "home" item
|
||||
return directus.request(readItems("home")) as unknown as HeroSection;
|
||||
}
|
||||
66
lib/query/post.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { directus } from "@/lib/directus";
|
||||
import { ItemsQuery, Post } from "@/types";
|
||||
import { readItem, readItems } from "@directus/sdk";
|
||||
|
||||
// Construct base URL for assets from environment variable
|
||||
const assetsUrl = `${process.env.DIRECTUS_API_ENDPOINT}/assets/`;
|
||||
|
||||
function getFullImageUrl(imageId: string | null | undefined): string | null {
|
||||
if (!imageId || !assetsUrl) {
|
||||
return null;
|
||||
}
|
||||
return `${assetsUrl}${imageId}`;
|
||||
}
|
||||
|
||||
// Function to fetch all published posts
|
||||
export async function getPosts(): Promise<Post[]> {
|
||||
try {
|
||||
const postsData = await directus.request(
|
||||
readItems("posts", {
|
||||
fields: [
|
||||
"slug",
|
||||
"title",
|
||||
"excerpt",
|
||||
"featured_image",
|
||||
"date_created",
|
||||
"content",
|
||||
],
|
||||
filter: {
|
||||
status: { _eq: "published" },
|
||||
},
|
||||
sort: ["-date_created"],
|
||||
})
|
||||
);
|
||||
|
||||
const posts = postsData.map((post) => ({
|
||||
...post,
|
||||
featured_image: getFullImageUrl(post.featured_image),
|
||||
})) as Post[];
|
||||
|
||||
return posts;
|
||||
} catch (error) {
|
||||
console.error("Error fetching posts:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Function to fetch a single post by slug
|
||||
export async function getPostBySlug(
|
||||
slug: string,
|
||||
options?: ItemsQuery
|
||||
): Promise<Post | null> {
|
||||
try {
|
||||
const postData = await directus.request(readItem("posts", slug, options));
|
||||
|
||||
// Map data to include full image URL
|
||||
const post = {
|
||||
...postData,
|
||||
featured_image: getFullImageUrl(postData.featured_image),
|
||||
} as Post; // Adjust cast if needed
|
||||
|
||||
return post;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching post with slug ${slug}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,10 @@ const nextConfig: NextConfig = {
|
||||
protocol: "https",
|
||||
hostname: "storage.cvevolve.com",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: process.env.DIRECTUS_API_ENDPOINT!.replace("https://", ""),
|
||||
},
|
||||
],
|
||||
},
|
||||
experimental: {
|
||||
|
||||
60
package-lock.json
generated
@ -9,8 +9,11 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@auth/prisma-adapter": "^2.9.0",
|
||||
"@directus/sdk": "^18.0.3",
|
||||
"@google/generative-ai": "^0.24.0",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@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",
|
||||
@ -18,6 +21,7 @@
|
||||
"nodemailer": "^6.10.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.56.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-spinners": "^0.16.1",
|
||||
"react-toggle-dark-mode": "^1.1.1",
|
||||
@ -646,6 +650,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",
|
||||
@ -1251,6 +1267,18 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.0.1.tgz",
|
||||
"integrity": "sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/utils": "^0.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-hook-form": "^7.55.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@ -2576,6 +2604,12 @@
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
@ -4604,6 +4638,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",
|
||||
@ -8821,6 +8865,22 @@
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.56.1",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.1.tgz",
|
||||
"integrity": "sha512-qWAVokhSpshhcEuQDSANHx3jiAEFzu2HAaaQIzi/r9FNPm1ioAvuJSD4EuZzWd7Al7nTRKcKPnBKO7sRn+zavQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-hook-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
||||
|
||||
@ -10,8 +10,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/prisma-adapter": "^2.9.0",
|
||||
"@directus/sdk": "^18.0.3",
|
||||
"@google/generative-ai": "^0.24.0",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@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",
|
||||
@ -19,6 +22,7 @@
|
||||
"nodemailer": "^6.10.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.56.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-spinners": "^0.16.1",
|
||||
"react-toggle-dark-mode": "^1.1.1",
|
||||
|
||||
BIN
public/images/absa.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
public/images/bcx.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
public/images/dashboard.png
Normal file
|
After Width: | Height: | Size: 277 KiB |
BIN
public/images/jacaranda.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
9
public/images/meeting.svg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
16
public/images/obse.svg
Normal file
|
After Width: | Height: | Size: 731 KiB |
BIN
public/images/salesforce.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
public/images/sasol.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
13
public/images/sybrin.svg
Normal file
|
After Width: | Height: | Size: 228 KiB |
9
public/images/team.svg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
public/images/toyota-logo.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
public/images/toyota.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
117
types/index.d.ts
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
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[];
|
||||
}
|
||||
|
||||
export interface Vacancy {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
department: string;
|
||||
location: {
|
||||
city: string;
|
||||
country: string;
|
||||
remote: boolean;
|
||||
};
|
||||
employmentType: "Full-time" | "Part-time" | "Contract" | "Internship";
|
||||
experienceLevel:
|
||||
| "Entry-level"
|
||||
| "Mid-level"
|
||||
| "Senior-level"
|
||||
| "Lead"
|
||||
| "Manager";
|
||||
salary?: {
|
||||
min: number;
|
||||
max: number;
|
||||
currency: string;
|
||||
period: "Annual" | "Monthly" | "Hourly";
|
||||
};
|
||||
responsibilities: string[];
|
||||
requiredQualifications: string[];
|
||||
preferredQualifications?: string[];
|
||||
benefits?: string[];
|
||||
applicationDeadline?: string; // ISO 8601 date string
|
||||
postedDate: string; // ISO 8601 date string
|
||||
contactPerson?: {
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
};
|
||||
status: "Open" | "Closed" | "Filled";
|
||||
}
|
||||