Home page completed

This commit is contained in:
libertyoms
2025-04-20 18:19:43 +02:00
parent 211cb3fc6f
commit 5eb60015cc
24 changed files with 1412 additions and 428 deletions

View File

@ -0,0 +1,43 @@
// components/CallToActionSection.tsx
import React from "react";
import Button from "@/components/ui/Button";
type CallToActionSectionProps = {
title: string;
subtitle: string;
buttonText: string;
buttonHref: string;
};
const CallToActionSection: React.FC<CallToActionSectionProps> = ({
title,
subtitle,
buttonText,
buttonHref,
}) => {
return (
// Use primary background, primary-foreground for text
<section className="bg-primary text-primary-foreground py-16 md:py-20">
{" "}
{/* Adjusted padding */}
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
{" "}
{/* Use container */}
<h2 className="text-3xl md:text-4xl font-bold mb-4">{title}</h2>{" "}
{/* Text color inherited */}
<p className="text-lg mb-8 max-w-xl mx-auto opacity-90">
{subtitle}
</p>{" "}
{/* Slightly less emphasis */}
{/* Button needs contrast on primary bg. Use a secondary/outline/custom variant */}
<Button href={buttonHref} variant="secondary" size="lg">
{/* Example: Using 'secondary' which uses light/dark gray bg defined in globals */}
{/* OR custom class: className="bg-background text-foreground hover:bg-background/90" */}
{buttonText}
</Button>
</div>
</section>
);
};
export default CallToActionSection;

View File

@ -0,0 +1,84 @@
// 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
// Define structure for client data (adapt as needed)
type Client = {
name: string;
logoUrl?: string; // URL to actual logo image
icon?: React.ElementType; // Placeholder icon component
};
type ClientLogosSectionProps = {
title: string;
description?: string;
clients: Client[];
};
const ClientLogosSection: React.FC<ClientLogosSectionProps> = ({
title,
description,
clients,
}) => {
return (
// Use semantic background
<section className="py-16 md:py-20 bg-background">
<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
key={index}
title={client.name}
className="transition-opacity hover:opacity-100"
>
{client.logoUrl ? (
<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
/>
) : 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>
{description && (
<p className="mt-8 text-muted-foreground italic text-sm">
{description}
</p>
)}
</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 },
];
export default ClientLogosSection;

View File

@ -0,0 +1,76 @@
// components/CoreServicesSection.tsx
import React from "react";
import ServiceCard from "./ServiceCard";
import { FaCogs, FaProjectDiagram, FaCode } from "react-icons/fa"; // Example icons
// Define the structure for a service item
type ServiceItem = {
icon: React.ElementType;
title: string;
description: string;
};
// Define props for the section
type CoreServicesSectionProps = {
title: string;
subtitle: string;
services: ServiceItem[];
};
const CoreServicesSection: React.FC<CoreServicesSectionProps> = ({
title,
subtitle,
services,
}) => {
return (
// Use semantic secondary background
<section className="py-20 md:py-28 bg-secondary">
<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-4 text-foreground">
{title}
</h2>
{/* Use semantic muted foreground */}
<p className="text-lg text-muted-foreground mb-12 md:mb-16 max-w-2xl mx-auto">
{subtitle}
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12">
{services.map((service, index) => (
<ServiceCard
key={index}
icon={service.icon}
title={service.title}
description={service.description}
/>
))}
</div>
</div>
</section>
);
};
// Example default data matching the original page
export const defaultCoreServices: ServiceItem[] = [
{
icon: FaCogs,
title: "Resource Augmentation",
description:
"Access top-tier IT talent seamlessly integrated with your team. Skilled professionals for short-term projects or long-term engagements.",
},
{
icon: FaProjectDiagram,
title: "Project Management",
description:
"Expert management ensuring on-time, within-budget delivery with superior results, risk mitigation, and maximum efficiency.",
},
{
icon: FaCode,
title: "Product Development",
description:
"Creating innovative, scalable digital products from concept to deployment to enhance efficiency and foster business growth.",
},
];
export default CoreServicesSection;

View File

@ -0,0 +1,108 @@
// components/FeaturedProductSection.tsx
import React from "react";
import Image from "next/image";
import Button from "@/components/ui/Button";
import { FiCheckCircle, FiArrowRight } from "react-icons/fi"; // Example icons
type FeaturePoint = {
text: string;
};
type FeaturedProductProps = {
eyebrow?: string;
title: string;
productName: string; // e.g., "OBSE"
description: string;
features: FeaturePoint[];
buttonText: string;
buttonHref: string;
imageUrl: string; // Path to product graphic/mockup
imageAlt: string;
};
const FeaturedProductSection: React.FC<FeaturedProductProps> = ({
eyebrow,
title,
productName,
description,
features,
buttonText,
buttonHref,
imageUrl,
imageAlt,
}) => {
return (
// Use secondary background for visual separation
<section className="py-20 md:py-28 bg-secondary overflow-hidden">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row items-center gap-12 md:gap-16 lg:gap-20">
{/* Text Content Area (Takes up half on desktop) */}
<div className="md:w-1/2 text-center md:text-left">
{eyebrow && (
<p className="text-sm font-semibold uppercase tracking-wider text-primary mb-3">
{eyebrow}
</p>
)}
{/* Main title uses foreground color */}
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-4 text-foreground">
{title} <span className="text-primary">{productName}</span>
</h2>
{/* Description uses muted foreground */}
<p className="text-lg text-muted-foreground mb-6">{description}</p>
{/* Feature List */}
<ul className="space-y-3 mb-8 text-left">
{" "}
{/* Ensure list is left-aligned */}
{features.map((feature, index) => (
<li key={index} className="flex items-start">
<FiCheckCircle className="w-5 h-5 text-primary mr-3 mt-1 flex-shrink-0" />
{/* Feature text uses muted foreground */}
<span className="text-muted-foreground">{feature.text}</span>
</li>
))}
</ul>
{/* Call to Action Button */}
<Button
href={buttonHref}
variant="primary"
size="lg"
className="group"
>
{buttonText}
<FiArrowRight className="ml-2 transition-transform duration-300 group-hover:translate-x-1" />
</Button>
</div>
{/* Image Area (Takes up half on desktop) */}
<div className="md:w-1/2 relative flex justify-center">
{/* Add perspective/shadow for visual lift */}
<div className="relative w-full max-w-md lg:max-w-lg xl:max-w-xl rounded-lg shadow-2xl overflow-hidden transform transition-transform duration-500 hover:scale-105">
{/* Apply subtle dark overlay on image if needed for contrast */}
{/* <div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent dark:from-black/20 z-10"></div> */}
<Image
src={imageUrl}
alt={imageAlt}
width={600} // Adjust intrinsic size
height={450} // Adjust intrinsic size
layout="responsive" // Makes image scale
objectFit="cover" // Or 'contain' depending on image
className="relative z-0"
/>
</div>
</div>
</div>
</div>
</section>
);
};
// Example default data for OBSE
export const defaultObseFeatures: FeaturePoint[] = [
{ text: "Automate data extraction & analysis from bank statements." },
{ text: "Reduce manual errors and increase processing speed." },
{ text: "Gain deep insights into financial health and trends." },
{ text: "Enhance fraud detection capabilities." },
{ text: "Seamless integration with existing financial systems." },
];
export default FeaturedProductSection;

View File

@ -0,0 +1,67 @@
// components/HeroSection.tsx
import React from "react";
import Image from "next/image";
import Button from "@/components/ui/Button"; // Use the updated Button
type HeroSectionProps = {
title: React.ReactNode; // Allow JSX like <br/>
subtitle: string;
buttonText: string;
buttonHref: string;
imageUrl?: string; // Optional image URL
// videoUrl?: string; // Optional video URL
};
const HeroSection: React.FC<HeroSectionProps> = ({
title,
subtitle,
buttonText,
buttonHref,
imageUrl = "/hero-bg.jpg", // Default background image
}) => {
return (
<section className="relative h-[70vh] md:h-[85vh] flex items-center justify-center text-center bg-gradient-to-b from-black/10 to-black/40 text-white overflow-hidden">
{" "}
{/* Adjusted background */}
{/* Background Image/Video */}
<div className="absolute inset-0 z-0 opacity-40 dark:opacity-30">
{" "}
{/* Adjusted opacity */}
{imageUrl && (
<Image
src={imageUrl}
alt="Hero background"
layout="fill"
objectFit="cover"
quality={75}
priority // Load hero image quickly
/>
)}
{/* TODO: Add video support if needed */}
</div>
{/* Overlay for better text contrast */}
<div className="absolute inset-0 bg-black/60 z-10"></div>
{/* Content */}
<div className="relative z-20 container mx-auto px-4 sm:px-6 lg:px-8">
{" "}
{/* Use container */}
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold mb-4 leading-tight text-primary animate-fade-in-up">
{title}
</h1>
<p className="text-lg md:text-xl max-w-3xl mx-auto mb-8 text-gray-200 dark:text-gray-300 animate-fade-in-up animation-delay-300">
{subtitle}
</p>
<Button
href={buttonHref}
variant="primary" // Use primary variant defined in Button component
size="lg"
className="animate-fade-in-up animation-delay-600"
>
{buttonText} {/* Simple arrow */}
</Button>
</div>
</section>
);
};
export default HeroSection;

View File

@ -0,0 +1,98 @@
// components/HeroSectionDynamic.tsx
import React from "react";
import Image from "next/image";
import Button from "@/components/ui/Button"; // Use the updated Button
import { FiArrowRight } from "react-icons/fi";
type HeroSectionProps = {
title: React.ReactNode; // Allow JSX like <br/>
subtitle: string;
buttonText: string;
buttonHref: string;
imageUrl?: string; // Main background visual
};
const HeroSectionDynamic: React.FC<HeroSectionProps> = ({
title,
subtitle,
buttonText,
buttonHref,
imageUrl = "/hero-bg.jpg", // Ensure this high-quality image exists
}) => {
return (
<section className="relative flex items-center bg-background min-h-screen overflow-hidden">
{/* Layer 1: Background Image/Gradient */}
<div className="absolute inset-0 z-0">
{imageUrl && (
<Image
src={imageUrl}
alt="Innovative Technology Background"
layout="fill"
objectFit="cover"
quality={85}
priority
className="opacity-40 dark:opacity-30" // Slightly dim the image
/>
)}
{/* Fallback gradient if no image */}
{/* <div className="absolute inset-0 bg-gradient-to-br from-secondary via-background to-background"></div> */}
{/* Subtle Vignette Effect */}
<div className="absolute inset-0 z-10 bg-gradient-radial from-transparent via-transparent to-background/60 dark:to-background/80"></div>
</div>
{/* Layer 2: Floating Abstract Shapes (Subtle animation) */}
{/* Shape 1 - Soft Primary Color Blob */}
<div
aria-hidden="true"
className="absolute top-[10%] left-[5%] w-48 h-48 md:w-72 md:h-72 bg-primary/10 dark:bg-primary/15 rounded-full filter blur-3xl opacity-70 dark:opacity-50 animate-float animation-delay-300 z-10"
></div>
{/* Shape 2 - Outline Shape */}
<div
aria-hidden="true"
className="absolute bottom-[15%] right-[8%] w-40 h-40 md:w-56 md:h-56 border-2 border-primary/30 dark:border-primary/40 rounded-lg opacity-50 animate-drift z-10 transform rotate-12 hidden lg:block"
></div>
{/* Shape 3 - Small Accent */}
<div
aria-hidden="true"
className="absolute top-[25%] right-[15%] w-12 h-12 bg-secondary dark:bg-secondary/50 rounded-full opacity-60 animate-pulse-slow z-10 hidden md:block"
></div>
{/* Layer 3: Content */}
<div className="relative z-20 container mx-auto px-4 sm:px-6 lg:px-8 py-24 md:py-32">
<div className="max-w-2xl lg:max-w-3xl text-center md:text-left">
{" "}
{/* Max width for content */}
{/* Optional: Small "Eyebrow" text */}
<p className="text-sm font-semibold uppercase tracking-wider text-primary mb-3 animate-fade-in-up">
Owethu Managed Services
</p>
{/* Title - Larger, bolder */}
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-extrabold mb-6 leading-tight text-foreground animate-fade-in-up animation-delay-300">
{/* Example gradient text - remove class if not desired */}
<span className="bg-gradient-to-r from-primary via-primary/80 to-foreground/80 dark:to-foreground/90 bg-clip-text text-transparent">
{title}
</span>
</h1>
{/* Subtitle - Clear foreground color */}
<p className="text-lg md:text-xl lg:text-2xl max-w-xl text-foreground/80 dark:text-foreground/70 mb-10 animate-fade-in-up animation-delay-600">
{subtitle}
</p>
{/* Button - Primary variant */}
<div className="animate-fade-in-up animation-delay-900">
<Button
href={buttonHref}
variant="primary"
size="lg"
className="group shadow-lg hover:shadow-primary/30 dark:hover:shadow-primary/20 transform transition-all duration-300 hover:scale-105" // Enhanced hover
>
{buttonText}
<FiArrowRight className="ml-2 transition-transform duration-300 group-hover:translate-x-1" />
</Button>
</div>
</div>
</div>
</section>
);
};
export default HeroSectionDynamic;

View File

@ -0,0 +1,80 @@
// components/HeroSectionModern.tsx
import React from "react";
import Image from "next/image";
import Button from "@/components/ui/Button"; // Use the updated Button
type HeroSectionProps = {
title: React.ReactNode; // Allow JSX like <br/>
subtitle: string;
buttonText: string;
buttonHref: string;
imageUrl?: string; // Optional image URL
// videoUrl?: string; // Optional video URL
};
const HeroSectionModern: React.FC<HeroSectionProps> = ({
title,
subtitle,
buttonText,
buttonHref,
imageUrl = "/hero-bg.jpg", // Default background image - MAKE SURE THIS EXISTS
}) => {
return (
// Use min-h-screen for full viewport height adjust if needed
<section className="relative flex flex-col md:flex-row items-center bg-background min-h-[80vh] md:min-h-screen overflow-hidden">
{/* Background Image/Video Layer */}
<div className="absolute inset-0 z-0">
{imageUrl && (
<Image
src={imageUrl}
alt="OMS Hero background"
layout="fill"
objectFit="cover"
quality={80} // Slightly higher quality
priority
// Add subtle zoom/pan animation on load (optional)
className="animate-[scale_1.05s_ease-out_forwards]" // Requires scale keyframes in globals.css
/>
)}
{/* TODO: Add video support if needed */}
{/* Gradient Overlay - Stronger on left, fades towards right */}
{/* Adjust gradient stops and colors based on light/dark mode */}
<div className="absolute inset-0 z-10 bg-gradient-to-r from-background via-background/70 to-background/10 dark:from-background dark:via-background/80 dark:to-background/20"></div>
</div>
{/* Content Area (Takes up roughly half on desktop) */}
<div className="relative z-20 w-full md:w-1/2 lg:w-3/5 h-full flex items-center py-20 md:py-0">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 md:px-12 text-center md:text-left">
{" "}
{/* Container for padding */}
{/* Title - Using Primary color */}
<h1 className="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-bold mb-5 md:mb-6 leading-tight text-primary animate-fade-in-up">
{title}
</h1>
{/* Subtitle - Using Foreground color */}
<p className="text-lg sm:text-xl lg:text-2xl max-w-xl mx-auto md:mx-0 mb-8 md:mb-10 text-foreground/90 dark:text-foreground/80 animate-fade-in-up animation-delay-300">
{subtitle}
</p>
{/* Button */}
<div className="animate-fade-in-up animation-delay-600">
<Button
href={buttonHref}
variant="primary"
size="lg"
className="shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-300" // Added hover effect
>
{buttonText}
</Button>
</div>
</div>
</div>
{/* Optional: Right side visual area (mostly shows background now) */}
{/* You could add abstract shapes or other elements here if desired */}
{/* <div className="hidden md:block md:w-1/2 lg:w-2/5 h-full"></div> */}
</section>
);
};
export default HeroSectionModern;

View File

@ -0,0 +1,27 @@
// components/ServiceCard.tsx
import React from "react";
type ServiceCardProps = {
icon: React.ElementType; // Expect a component like FaCogs
title: string;
description: string;
};
const ServiceCard: React.FC<ServiceCardProps> = ({
icon: Icon,
title,
description,
}) => {
return (
// Use semantic variables for background, text, border
<div className="bg-card p-8 rounded-lg border border-border shadow-md hover:shadow-xl transition-shadow duration-300 text-left">
<Icon className="text-primary text-4xl mb-4" />
{/* Use semantic foreground for title */}
<h3 className="text-xl font-semibold mb-3 text-foreground">{title}</h3>
{/* Use semantic muted foreground for description */}
<p className="text-muted-foreground">{description}</p>
</div>
);
};
export default ServiceCard;

View File

@ -0,0 +1,97 @@
// components/WhyChooseUsSection.tsx
import React from "react";
import { IconType } from "react-icons"; // Import IconType for typing
import { FiZap, FiUsers, FiTarget, FiBarChart2 } from "react-icons/fi"; // Example icons
type FeatureItem = {
icon: IconType;
title: string;
description: string;
};
type WhyChooseUsProps = {
title: string;
subtitle?: string;
features: FeatureItem[];
};
const WhyChooseUsSection: React.FC<WhyChooseUsProps> = ({
title,
subtitle,
features,
}) => {
return (
// Use standard background for seamless flow or secondary for slight distinction
<section className="py-20 md:py-28 bg-background">
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto text-center mb-12 md:mb-16">
{/* Use semantic foreground */}
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-foreground">
{title}
</h2>
{subtitle && (
// Use semantic muted foreground
<p className="text-lg text-muted-foreground">{subtitle}</p>
)}
</div>
{/* Features Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8 md:gap-10">
{features.map((feature, index) => (
<div
key={index}
className="text-center md:text-left flex flex-col items-center md:items-start p-6 rounded-lg transition-colors duration-300 hover:bg-secondary"
>
{" "}
{/* Added padding and hover */}
<div className="flex-shrink-0 mb-4">
{/* Icon using primary color */}
<feature.icon className="w-10 h-10 text-primary" />
</div>
<div>
{/* Title using foreground */}
<h3 className="text-xl font-semibold mb-2 text-foreground">
{feature.title}
</h3>
{/* Description using muted foreground */}
<p className="text-base text-muted-foreground">
{feature.description}
</p>
</div>
</div>
))}
</div>
</div>
</section>
);
};
// Example default data (adapt based on OMS's key selling points/values)
export const defaultWhyChooseUsFeatures: FeatureItem[] = [
{
icon: FiZap,
title: "Innovation Driven",
description:
"We leverage cutting-edge technology to create transformative solutions that push boundaries.",
},
{
icon: FiUsers,
title: "Expert Teams",
description:
"Access highly skilled IT professionals tailored to your project needs, ensuring expertise and quality.",
},
{
icon: FiTarget,
title: "Client-Centric Approach",
description:
"Your success is our priority. We partner closely with you to deliver solutions aligned with your goals.",
},
{
icon: FiBarChart2,
title: "Measurable Results",
description:
"Our focus is on delivering tangible business impact, enhancing efficiency and driving growth.",
},
];
export default WhyChooseUsSection;

94
app/(website)/page.tsx Normal file
View File

@ -0,0 +1,94 @@
import HeroSection from "./_components/HeroSection"; // Import the HeroSection component
import ClientLogosSection, {
defaultClients,
} from "./_components/ClientLogosSection"; // Import component and data
import CallToActionSection from "./_components/CallToActionSection";
import CoreServicesSection, {
defaultCoreServices,
} from "./_components/CoreServicesSection";
import WhyChooseUsSection, {
defaultWhyChooseUsFeatures,
} from "./_components/WhyChooseUsSection";
import FeaturedProductSection, {
defaultObseFeatures,
} from "./_components/FeaturedProductSection";
// import HeroSectionModern from "./_components/HeroSectionModern";
// import HeroSectionDynamic from "./_components/HeroSectionDynamic";
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.
</>
}
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
/>
<CoreServicesSection
title="Core Services"
subtitle="Tailored solutions designed to drive growth, optimize productivity, and solve your most complex business challenges."
services={defaultCoreServices} // Pass the data
/>
<WhyChooseUsSection
title="Why Partner with OMS?"
subtitle="Combining expertise with a commitment to excellence for your success."
features={defaultWhyChooseUsFeatures}
/>
<FeaturedProductSection
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."
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**
imageAlt="OBSE Product Interface Mockup"
/>
<ClientLogosSection
title="Trusted By Industry Leaders"
clients={defaultClients} // Pass placeholder data
description="Showcasing key clients across financial services, automotive, and tech industries." // Optional description
/>
{/* TODO: Implement actual client logo fetching and display */}
{/* TODO: Add auto-sliding carousel for clients */}
<CallToActionSection
title="Ready to Innovate?"
subtitle="Let's discuss how OMS can help transform your business with cutting-edge technology solutions."
buttonText="Get In Touch"
buttonHref="/contact"
/>
</>
);
}

View File

@ -0,0 +1,2 @@
import { handlers } from "@/auth"; // Referring to the auth.ts we just created
export const { GET, POST } = handlers;

View File

@ -131,6 +131,67 @@
animation-delay: 0.6s; animation-delay: 0.6s;
} }
} }
/* Optional scale animation for background */
@keyframes floatSlightly {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
@keyframes drift {
0% {
transform: translate(0, 0) rotate(0deg);
}
25% {
transform: translate(5px, -5px) rotate(2deg);
}
50% {
transform: translate(0, 0) rotate(0deg);
}
75% {
transform: translate(-5px, 5px) rotate(-2deg);
}
100% {
transform: translate(0, 0) rotate(0deg);
}
}
@keyframes subtlePulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.85;
}
}
/* --- Animation Utility Classes (Add if not already present) --- */
@layer utilities {
.animate-float {
animation: floatSlightly 6s ease-in-out infinite;
}
.animate-drift {
animation: drift 15s ease-in-out infinite;
}
.animate-pulse-slow {
animation: subtlePulse 5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Keep existing animation delays */
.animation-delay-300 {
animation-delay: 0.3s;
}
.animation-delay-600 {
animation-delay: 0.6s;
}
.animation-delay-900 {
animation-delay: 0.9s;
} /* Added longer delay */
}
@keyframes fadeInUp { @keyframes fadeInUp {
from { from {
@ -145,5 +206,5 @@
.animate-fade-in-up { .animate-fade-in-up {
animation: fadeInUp 0.8s ease-out forwards; animation: fadeInUp 0.8s ease-out forwards;
opacity: 0; opacity: 0; /* Start hidden */
} }

View File

@ -1,4 +1,3 @@
// app/layout.tsx
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Poppins } from "next/font/google"; import { Poppins } from "next/font/google";
import "./globals.css"; import "./globals.css";
@ -13,11 +12,73 @@ const poppins = Poppins({
variable: "--font-poppins", variable: "--font-poppins",
}); });
// --- Define Metadata ---
export const metadata: Metadata = { export const metadata: Metadata = {
title: "OMS - Owethu Managed Services", title:
description: "Where innovation meets excellence.", "OMS: IT Solutions, Resource Augmentation & Product Development | Owethu Managed Services", // Primary Keywords first
description:
"Owethu Managed Services (OMS) provides expert IT solutions in Centurion & South Africa, including resource augmentation, project management, custom software development, and the OBSE financial analysis tool.", // Include Keywords, Location, USP
keywords: [
"Owethu Managed Services",
"OBSE",
"IT solutions South Africa",
"resource augmentation",
"project management IT",
"custom software development",
"OBSE",
"financial data analysis",
"IT services Centurion",
], // Add relevant keywords
alternates: {
canonical: "/", // Assuming this is the root URL
},
openGraph: {
// For social media sharing
title:
"OMS: Leading IT Solutions & Resource Augmentation | Owethu Managed Services",
description:
"Partner with OMS for innovative IT services, expert talent, and transformative digital products like OBSE.",
url: "https://oms.africa",
siteName: "Owethu Managed Services (OMS)",
images: [
{
url: "/og-image.jpg", // Create a compelling OG image (e.g., 1200x630) and place in /public
width: 1200,
height: 630,
alt: "Owethu Managed Services - Innovation Meets Excellence",
},
],
locale: "en_ZA",
type: "website",
},
twitter: {
// For Twitter cards
card: "summary_large_image",
title: "OMS: IT Solutions & Services | Owethu Managed Services",
description:
"Expert resource augmentation, project management, and product development including OBSE.",
// creator: '@YourTwitterHandle', // Optional: Add your Twitter handle
images: ["/og-image.jpg"], // Use the same OG image
},
robots: {
// Instruct search engine crawlers
index: true, // Allow indexing of this page
follow: true, // Allow following links from this page
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
// verification: { // Add verification codes if needed
// google: 'YOUR_GOOGLE_VERIFICATION_CODE',
// yandex: 'YOUR_YANDEX_VERIFICATION_CODE',
// other: { me: ['my-email@example.com', 'my-link'] },
// },
}; };
// --- End Metadata ---
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{

12
app/loading.tsx Normal file
View File

@ -0,0 +1,12 @@
"use client";
import { PacmanLoader } from "react-spinners";
const Loading = () => {
return (
<div className="flex items-center justify-center h-screen">
<PacmanLoader />
</div>
);
};
export default Loading;

View File

@ -1,168 +0,0 @@
// app/page.tsx
import Image from "next/image";
import Button from "@/components/ui/Button"; // Use the reusable button
import {
FaCogs,
FaProjectDiagram,
FaCode,
FaUsers,
FaBuilding,
FaCar,
FaLaptopCode,
} from "react-icons/fa"; // Import icons
export default function HomePage() {
return (
<>
{/* Hero Section */}
<section className="relative h-[70vh] md:h-[85vh] flex items-center justify-center text-center bg-dark text-light overflow-hidden">
{/* Background Video/Image Placeholder */}
{/* TODO: Replace with actual <video> or Next/Image */}
<div className="absolute inset-0 z-0 opacity-30">
<Image
src="/hero-bg.jpg" // Add a placeholder image in /public
alt="Digital transformation background"
layout="fill"
objectFit="cover"
quality={75}
/>
{/* OR a video element:
<video
autoPlay
loop
muted
playsInline
className="w-full h-full object-cover"
poster="/placeholder-hero-bg.jpg" // Poster image
>
<source src="/your-video.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video> */}
</div>
{/* Overlay */}
<div className="absolute inset-0 bg-gradient-to-b from-black/50 to-black/80 z-10"></div>
{/* Content */}
<div className="relative z-20 container mx-auto px-6">
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold mb-4 leading-tight text-primary animate-fade-in-up">
Where Innovation <br className="hidden md:block" /> Meets
Excellence.
</h1>
<p className="text-lg md:text-xl max-w-3xl mx-auto mb-8 text-gray-300 animate-fade-in-up animation-delay-300">
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.
</p>
<Button
href="/about"
size="lg"
className="animate-fade-in-up animation-delay-600"
>
Learn More
</Button>
</div>
</section>
{/* Core Services Section */}
<section className="py-20 md:py-28 bg-light-gray">
<div className="container mx-auto px-6 text-center">
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-dark">
Core Services
</h2>
<p className="text-lg text-gray-600 mb-12 md:mb-16 max-w-2xl mx-auto">
Tailored solutions designed to drive growth, optimize productivity,
and solve your most complex business challenges.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12">
{/* Service Card 1: Resource Augmentation */}
<div className="bg-light p-8 rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 text-left">
<FaCogs className="text-primary text-4xl mb-4" />
<h3 className="text-xl font-semibold mb-3 text-dark">
Resource Augmentation
</h3>
<p className="text-gray-600">
Access top-tier IT talent seamlessly integrated with your team.
Skilled professionals for short-term projects or long-term
engagements.
</p>
</div>
{/* Service Card 2: Project Management */}
<div className="bg-light p-8 rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 text-left">
<FaProjectDiagram className="text-primary text-4xl mb-4" />
<h3 className="text-xl font-semibold mb-3 text-dark">
Project Management
</h3>
<p className="text-gray-600">
Expert management ensuring on-time, within-budget delivery with
superior results, risk mitigation, and maximum efficiency.
</p>
</div>
{/* Service Card 3: Product Development */}
<div className="bg-light p-8 rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 text-left">
<FaCode className="text-primary text-4xl mb-4" />
<h3 className="text-xl font-semibold mb-3 text-dark">
Product Development
</h3>
<p className="text-gray-600">
Creating innovative, scalable digital products from concept to
deployment to enhance efficiency and foster business growth.
</p>
</div>
</div>
</div>
</section>
{/* Client Logos Section */}
<section className="py-16 md:py-20 bg-light">
<div className="container mx-auto px-6 text-center">
<h2 className="text-3xl md:text-4xl font-bold mb-12 text-dark">
Trusted By Industry Leaders
</h2>
{/* TODO: Implement Auto-Sliding Panel (e.g., using Swiper.js or react-slick) */}
<div className="flex flex-wrap justify-center items-center gap-10 md:gap-16 opacity-70">
{/* Placeholder Logos - Replace with actual client logos */}
<FaBuilding
title="Financial Services Client"
className="text-5xl text-gray-500 hover:text-primary transition-colors"
/>
<FaCar
title="Automotive Client"
className="text-5xl text-gray-500 hover:text-primary transition-colors"
/>
<FaLaptopCode
title="Tech Industry Client"
className="text-5xl text-gray-500 hover:text-primary transition-colors"
/>
<FaUsers
title="Generic Client 1"
className="text-5xl text-gray-500 hover:text-primary transition-colors"
/>
<FaBuilding
title="Generic Client 2"
className="text-5xl text-gray-500 hover:text-primary transition-colors"
/>
</div>
<p className="mt-8 text-gray-500 italic">
Showcasing key clients across financial services, automotive, and
tech industries.
{/* (Auto-sliding panel coming soon!) */}
</p>
</div>
</section>
{/* Call to Action (Optional but good practice) */}
<section className="bg-primary text-dark py-16">
<div className="container mx-auto px-6 text-center">
<h2 className="text-3xl font-bold mb-4">Ready to Innovate?</h2>
<p className="text-lg mb-8 max-w-xl mx-auto">
Let&apos;s discuss how OMS can help transform your business with
cutting-edge technology solutions.
</p>
<Button href="/contact" variant="secondary" size="lg">
Get In Touch
</Button>
</div>
</section>
</>
);
}

6
auth.ts Normal file
View File

@ -0,0 +1,6 @@
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [GitHub],
});

View File

@ -1,43 +1,68 @@
// components/Footer.tsx // components/Footer.tsx
import React from "react"; import React from "react";
import Link from "next/link"; import Link from "next/link";
import { FaLinkedin, FaInstagram } from "react-icons/fa"; // npm install react-icons import { FaLinkedin, FaInstagram } from "react-icons/fa";
import Image from "next/image"; import Image from "next/image";
const omsLogoUrl = "/oms-logo.svg";
const omsLogoUrl = "/oms-logo.svg"; // Ensure this exists in /public
// const omsLogoDarkUrl = "/oms-logo-dark.svg"; // Optional dark mode logo
const Footer = () => { const Footer = () => {
// In a real app, you might use useTheme to get the current theme
// import { useTheme } from 'next-themes';
// const { resolvedTheme } = useTheme();
// const currentLogo = resolvedTheme === 'dark' && omsLogoDarkUrl ? omsLogoDarkUrl : omsLogoUrl;
const currentLogo = omsLogoUrl; // Using default for now
return ( return (
<footer className="bg-dark text-light pt-16 pb-8"> // --- Force Dark Background ---
<div className="container mx-auto px-6"> // Use a specific dark color (e.g., var(--oms-black) or a dark gray from your vars)
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 mb-12"> // Text color will default to light here for contrast
{/* About/Logo */} <footer className="bg-[var(--oms-black)] text-[var(--oms-white)] pt-16 pb-8">
{" "}
{/* Or use --background from .dark */}
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 md:gap-12 mb-12">
{/* About/Logo Section */}
<div> <div>
<div className="flex items-center space-x-2 mb-4"> <div className="flex items-center space-x-2 mb-4">
<Image <Image
src={omsLogoUrl} // Use your actual logo path src={currentLogo}
alt="OMS Logo" alt="OMS Logo"
width={40} // Adjust size as needed width={40}
height={40} height={40}
priority // Load logo quickly priority
// Optional: Add filter for dark bg if logo isn't ideal
// className="dark:invert"
/> />
<span className="text-xl font-bold text-light">OMS</span> {/* Ensure prominent text is bright */}
<span className="text-xl font-bold text-[var(--oms-white)]">
OMS
</span>
</div> </div>
<p className="text-sm text-gray-400">Owethu Managed Services</p> {/* Use a slightly muted bright color for less prominent text */}
<p className="text-sm text-gray-400 mt-2"> <p className="text-sm text-[var(--oms-white)]/80">
{" "}
{/* Example: White with 80% opacity */}
Owethu Managed Services
</p>
<p className="text-sm text-[var(--oms-white)]/80 mt-2">
Where innovation meets excellence. Where innovation meets excellence.
</p> </p>
</div> </div>
{/* Quick Links */} {/* Quick Links Section */}
<div> <div>
{/* Use primary color for headings */}
<h5 className="text-lg font-semibold mb-4 text-primary"> <h5 className="text-lg font-semibold mb-4 text-primary">
Quick Links Quick Links
</h5> </h5>
{/* Use muted bright color for links, primary on hover */}
<ul className="space-y-2"> <ul className="space-y-2">
<li> <li>
<Link <Link
href="/about" href="/about"
className="hover:text-primary transition-colors duration-300" className="text-sm text-[var(--oms-white)]/80 hover:text-primary transition-colors"
> >
About Us About Us
</Link> </Link>
@ -45,7 +70,7 @@ const Footer = () => {
<li> <li>
<Link <Link
href="/services" href="/services"
className="hover:text-primary transition-colors duration-300" className="text-sm text-[var(--oms-white)]/80 hover:text-primary transition-colors"
> >
Services Services
</Link> </Link>
@ -53,7 +78,7 @@ const Footer = () => {
<li> <li>
<Link <Link
href="/products" href="/products"
className="hover:text-primary transition-colors duration-300" className="text-sm text-[var(--oms-white)]/80 hover:text-primary transition-colors"
> >
Products Products
</Link> </Link>
@ -61,7 +86,7 @@ const Footer = () => {
<li> <li>
<Link <Link
href="/join-us" href="/join-us"
className="hover:text-primary transition-colors duration-300" className="text-sm text-[var(--oms-white)]/80 hover:text-primary transition-colors"
> >
Join Our Team Join Our Team
</Link> </Link>
@ -69,7 +94,7 @@ const Footer = () => {
<li> <li>
<Link <Link
href="/contact" href="/contact"
className="hover:text-primary transition-colors duration-300" className="text-sm text-[var(--oms-white)]/80 hover:text-primary transition-colors"
> >
Contact Us Contact Us
</Link> </Link>
@ -77,29 +102,36 @@ const Footer = () => {
</ul> </ul>
</div> </div>
{/* Contact Info */} {/* Contact Info Section */}
<div> <div>
<h5 className="text-lg font-semibold mb-4 text-primary">Contact</h5> <h5 className="text-lg font-semibold mb-4 text-primary">Contact</h5>
<p className="text-sm text-gray-400 mb-2"> <address className="text-sm text-[var(--oms-white)]/80 mb-2 not-italic">
Unit 10 B Centuria Park Unit 10 B Centuria Park
<br /> <br />
265 Von Willich Avenue 265 Von Willich Avenue
<br /> <br />
Die Hoewes, Centurion, 0159 Die Hoewes, Centurion, 0159
</address>
<p className="text-sm text-[var(--oms-white)]/80 mb-2">
Phone:{" "}
<a href="tel:+27120513282" className="hover:text-primary">
(012) 051 3282
</a>
</p> </p>
<p className="text-sm text-gray-400 mb-2">Phone: (012) 051 3282</p> <p className="text-sm text-[var(--oms-white)]/80 mb-2">
<p className="text-sm text-gray-400 mb-2">
Email:{" "} Email:{" "}
<a href="mailto:hello@oms.africa" className="hover:text-primary"> <a href="mailto:hello@oms.africa" className="hover:text-primary">
hello@oms.africa hello@oms.africa
</a> </a>
</p> </p>
{/* Social Icons - Use muted bright color, primary on hover */}
<div className="flex space-x-4 mt-4"> <div className="flex space-x-4 mt-4">
<a <a
href="https://linkedin.com" href="https://linkedin.com"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-gray-400 hover:text-primary" className="text-[var(--oms-white)]/70 hover:text-primary transition-colors"
aria-label="LinkedIn"
> >
<FaLinkedin size={24} /> <FaLinkedin size={24} />
</a> </a>
@ -107,40 +139,45 @@ const Footer = () => {
href="https://instagram.com" href="https://instagram.com"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-gray-400 hover:text-primary" className="text-[var(--oms-white)]/70 hover:text-primary transition-colors"
aria-label="Instagram"
> >
<FaInstagram size={24} /> <FaInstagram size={24} />
</a> </a>
</div> </div>
</div> </div>
{/* Newsletter */} {/* Newsletter Section */}
<div> <div>
<h5 className="text-lg font-semibold mb-4 text-primary"> <h5 className="text-lg font-semibold mb-4 text-primary">
Newsletter Newsletter
</h5> </h5>
<p className="text-sm text-gray-400 mb-3"> <p className="text-sm text-[var(--oms-white)]/80 mb-3">
Stay updated with our latest news. Stay updated with our latest news.
</p> </p>
<form className="flex flex-col sm:flex-row gap-2"> <form className="flex flex-col sm:flex-row gap-2">
{/* Input needs dark background styles */}
<input <input
type="email" type="email"
placeholder="Enter your email" placeholder="Enter your email"
className="flex-grow px-4 py-2 rounded-md bg-gray-700 text-light border border-gray-600 focus:outline-none focus:ring-2 focus:ring-primary/50" aria-label="Email for newsletter"
// Use dark variable for input bg/border, bright for text
className="flex-grow px-4 py-2 rounded-lg bg-[var(--dark-input)] border border-[var(--dark-border)] text-[var(--dark-foreground)] placeholder:text-[var(--dark-muted-foreground)] focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-[var(--oms-black)]" // Ring offset needs dark bg
/> />
{/* Keep button styling primary */}
<button <button
type="submit" type="submit"
className="bg-primary text-dark px-4 py-2 rounded-md font-semibold hover:bg-primary/90 transition-colors duration-300" className="bg-primary text-primary-foreground px-4 py-2 rounded-lg font-semibold hover:bg-opacity-90 transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-[var(--oms-black)]" // Ring offset needs dark bg
> >
Subscribe Subscribe
</button> </button>
</form> </form>
{/* TODO: Add Badges */} {/* Badges - Use a subtle dark bg */}
<div className="mt-6 space-x-4"> <div className="mt-6 space-x-2 sm:space-x-4">
<span className="inline-block bg-gray-600 px-3 py-1 rounded text-xs font-semibold"> <span className="inline-block bg-[var(--dark-secondary)] text-[var(--dark-secondary-foreground)] px-3 py-1 rounded-md text-xs font-medium">
Salesforce Partner Salesforce Partner
</span> </span>
<span className="inline-block bg-gray-600 px-3 py-1 rounded text-xs font-semibold"> <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 X
</span> </span>
</div> </div>
@ -148,16 +185,24 @@ const Footer = () => {
</div> </div>
{/* Bottom Bar */} {/* Bottom Bar */}
<div className="border-t border-gray-700 pt-8 flex flex-col md:flex-row justify-between items-center text-sm text-gray-500"> {/* Use specific dark border, muted bright text */}
<p> <div className="border-t border-[var(--dark-border)] pt-8 flex flex-col md:flex-row justify-between items-center text-sm text-[var(--oms-white)]/70">
<p className="text-center md:text-left mb-4 md:mb-0">
© {new Date().getFullYear()} Owethu Managed Services. All Rights © {new Date().getFullYear()} Owethu Managed Services. All Rights
Reserved. Reserved.
</p> </p>
<div className="flex space-x-4 mt-4 md:mt-0"> <div className="flex space-x-4">
<Link href="/privacy-policy" className="hover:text-primary"> {/* Links still hover to primary */}
<Link
href="/privacy-policy"
className="hover:text-primary transition-colors"
>
Privacy Policy Privacy Policy
</Link> </Link>
<Link href="/paia-popia" className="hover:text-primary"> <Link
href="/paia-popia"
className="hover:text-primary transition-colors"
>
PAIA & POPIA PAIA & POPIA
</Link> </Link>
</div> </div>

View File

@ -4,7 +4,8 @@
import React, { useState } from "react"; import React, { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { usePathname } from "next/navigation"; // Use usePathname only if needed for active state logic not shown here
// import { usePathname } from "next/navigation";
import { import {
FiChevronDown, FiChevronDown,
FiClipboard, FiClipboard,
@ -12,11 +13,12 @@ import {
FiMenu, FiMenu,
FiX, FiX,
} from "react-icons/fi"; } from "react-icons/fi";
import ThemeToggle from "./ThemeToggle"; import ThemeToggle from "./ThemeToggle"; // Assuming ThemeToggle component exists
const omsLogoUrl = "/oms-logo.svg"; const omsLogoUrl = "/oms-logo.svg"; // Ensure this is in your /public folder
// --- Basic Dropdown Placeholder --- // --- Basic Dropdown Component ---
// Using semantic variables for background, text, and borders
type DropdownMenuProps = { type DropdownMenuProps = {
trigger: React.ReactNode; trigger: React.ReactNode;
children: React.ReactNode; children: React.ReactNode;
@ -27,16 +29,17 @@ const DropdownMenu = ({
children, children,
menuClasses = "w-48", menuClasses = "w-48",
}: DropdownMenuProps) => ( }: DropdownMenuProps) => (
// Using group-focus-within for better keyboard/touch accessibility
<div className="relative group"> <div className="relative group">
<button className="flex items-center space-x-1 text-sm font-medium focus:outline-none inherit-color group"> <button className="flex items-center space-x-1 text-sm font-medium focus:outline-none inherit-color group">
{trigger} {trigger}
<FiChevronDown className="w-4 h-4 transition-transform duration-200 group-hover:rotate-180" /> <FiChevronDown className="w-4 h-4 transition-transform duration-200 group-hover:rotate-180 group-focus-within:rotate-180" />
</button> </button>
<div <div
className={` className={`
absolute left-0 mt-2 ${menuClasses} origin-top-left rounded-md shadow-lg z-50 absolute left-0 mt-2 ${menuClasses} origin-top-left rounded-md shadow-lg z-50
bg-light border border-gray-200 dark:bg-[var(--color-dark-bg-secondary)] dark:border-[var(--color-dark-border)] bg-card border border-border {/* USE SEMANTIC VARS */}
opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 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> <div className="py-1">{children}</div>
@ -44,34 +47,40 @@ const DropdownMenu = ({
</div> </div>
); );
// Dropdown Link component using semantic variables
type DropdownLinkProps = { type DropdownLinkProps = {
href: string; href: string;
children: React.ReactNode; children: React.ReactNode;
onClick?: () => void; // Added onClick for mobile menu closure
}; };
const DropdownLink = ({ href, children }: DropdownLinkProps) => ( const DropdownLink = ({ href, children, onClick }: DropdownLinkProps) => (
<Link <Link
href={href} href={href}
className="block px-4 py-2 text-sm text-gray-700 dark:text-[var(--color-dark-text-secondary)] hover:bg-gray-100 hover:text-primary dark:hover:bg-gray-700 dark:hover:text-primary" 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
> >
{children} {children}
</Link> </Link>
); );
// --- End Dropdown Placeholder --- // --- End Dropdown Component ---
const Header = () => { const Header = () => {
const pathname = usePathname() || "/"; // If you need pathname for active states later:
const isActive = (path: string) => pathname === path; // const pathname = usePathname() || "/";
// const isActive = (path: string) => pathname === path;
// const isActive = (path: string) => false; // Simple placeholder
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const toggleMenu = () => setIsMenuOpen((open) => !open); const toggleMenu = () => setIsMenuOpen((open) => !open);
// Close menu when a link is clicked
const handleMobileLinkClick = () => setIsMenuOpen(false); const handleMobileLinkClick = () => setIsMenuOpen(false);
// Optionally switch logo based on a global CSS class or via next-themes
const currentLogo = omsLogoUrl; const currentLogo = omsLogoUrl;
return ( return (
<header className="sticky top-0 z-50 shadow-md bg-light dark:bg-dark-bg dark:border-b dark:border-dark-border"> // 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 */} {/* Top Row */}
<div className="container mx-auto px-4 sm:px-6 lg:px-8"> <div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16"> <div className="flex justify-between items-center h-16">
@ -88,40 +97,36 @@ const Header = () => {
height={40} height={40}
priority priority
/> />
<span className="text-xl font-bold text-dark dark:text-light hidden sm:inline"> {/* Use semantic variable for text color */}
<span className="text-xl font-bold text-foreground hidden sm:inline">
Owethu Managed Services Owethu Managed Services
</span> </span>
</Link> </Link>
{/* Desktop Navigation */} {/* Desktop Navigation */}
<nav className="hidden md:flex items-center space-x-6 lg:space-x-8"> <nav className="hidden md:flex items-center space-x-6 lg:space-x-8">
{/* Use semantic variables for text, hover uses primary */}
<Link <Link
href="/" href="/"
className={`text-sm font-medium ${ className={`text-sm font-medium text-foreground/80 hover:text-primary`}
isActive("/") /* Active state example (requires usePathname) */
? "text-primary font-semibold" /* ${isActive("/") ? "text-primary font-semibold" : "text-foreground/80 hover:text-primary"} */
: "text-gray-700 dark:text-[var(--color-dark-text-secondary)] hover:text-primary"
}`}
> >
Home Home
</Link> </Link>
<Link <Link
href="/about" href="/about"
className={`text-sm font-medium ${ className={`text-sm font-medium text-foreground/80 hover:text-primary`}
pathname.startsWith("/about") /* Active state example */
? "text-primary font-semibold" /* ${pathname.startsWith("/about") ? "text-primary font-semibold" : "text-foreground/80 hover:text-primary"} */
: "text-gray-700 dark:text-[var(--color-dark-text-secondary)] hover:text-primary"
}`}
> >
About Us About Us
</Link> </Link>
<Link <Link
href="/contact" href="/contact"
className={`text-sm font-medium ${ className={`text-sm font-medium text-foreground/80 hover:text-primary`}
isActive("/contact") /* Active state example */
? "text-primary font-semibold" /* ${isActive("/contact") ? "text-primary font-semibold" : "text-foreground/80 hover:text-primary"} */
: "text-gray-700 dark:text-[var(--color-dark-text-secondary)] hover:text-primary"
}`}
> >
Contact Us Contact Us
</Link> </Link>
@ -131,9 +136,10 @@ const Header = () => {
<div className="hidden md:flex items-center space-x-4"> <div className="hidden md:flex items-center space-x-4">
<ThemeToggle /> <ThemeToggle />
{/* Use semantic variables for button */}
<Link <Link
href="/request-demo" href="/request-demo"
className="flex items-center text-sm font-medium bg-primary text-dark px-3 py-1.5 rounded-md hover:bg-opacity-90 transition-colors" 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" // Use rounded-lg from radius
title="Request a Demo" title="Request a Demo"
> >
<FiClipboard className="w-4 h-4 mr-1.5" /> <FiClipboard className="w-4 h-4 mr-1.5" />
@ -143,12 +149,13 @@ const Header = () => {
{/* Mobile Buttons */} {/* Mobile Buttons */}
<div className="md:hidden flex items-center"> <div className="md:hidden flex items-center">
<ThemeToggle /> <ThemeToggle /> {/* Theme toggle appears first */}
<button <button
onClick={toggleMenu} onClick={toggleMenu}
className="text-gray-600 dark:text-gray-400 hover:text-primary dark:hover:text-primary focus:outline-none ml-3" className="text-foreground/60 hover:text-primary focus:outline-none ml-3" // Use semantic muted color, hover primary
aria-label="Toggle menu" aria-label="Toggle menu"
aria-expanded={isMenuOpen}
aria-controls="mobile-menu"
> >
{isMenuOpen ? ( {isMenuOpen ? (
<FiX className="w-6 h-6" /> <FiX className="w-6 h-6" />
@ -161,10 +168,16 @@ const Header = () => {
</div> </div>
{/* Secondary Row */} {/* Secondary Row */}
<div className="bg-primary dark:bg-primary"> <div className="bg-primary">
{" "}
{/* Keep gold background */}
<div className="container mx-auto px-4 sm:px-6 lg:px-8"> <div className="container mx-auto px-4 sm:px-6 lg:px-8">
{/* Hide on mobile */}
<div className="hidden md:flex justify-between items-center h-12"> <div className="hidden md:flex justify-between items-center h-12">
<nav className="flex items-center space-x-6 lg:space-x-8 text-dark"> {/* Use primary-foreground for text color */}
<nav className="flex items-center space-x-6 lg:space-x-8 text-primary-foreground">
{/* Wrap dropdown triggers for consistent hover */}
<div className="hover:text-opacity-80 transition-opacity">
<DropdownMenu trigger={<span>Services</span>}> <DropdownMenu trigger={<span>Services</span>}>
<DropdownLink href="/services/resource-augmentation"> <DropdownLink href="/services/resource-augmentation">
Resource Augmentation Resource Augmentation
@ -176,19 +189,27 @@ const Header = () => {
Product Development Product Development
</DropdownLink> </DropdownLink>
</DropdownMenu> </DropdownMenu>
</div>
<div className="hover:text-opacity-80 transition-opacity">
<DropdownMenu trigger={<span>Products</span>}> <DropdownMenu trigger={<span>Products</span>}>
<DropdownLink href="/products/obse">OBSE</DropdownLink> <DropdownLink href="/products/obse">OBSE</DropdownLink>
</DropdownMenu> </DropdownMenu>
</div>
<div className="hover:text-opacity-80 transition-opacity">
<DropdownMenu trigger={<span>Join Our Team</span>}> <DropdownMenu trigger={<span>Join Our Team</span>}>
<DropdownLink href="/join-us/vacancies">Vacancies</DropdownLink> <DropdownLink href="/join-us/vacancies">
Vacancies
</DropdownLink>
<DropdownLink href="/join-us/portal"> <DropdownLink href="/join-us/portal">
Recruitment Portal Recruitment Portal
</DropdownLink> </DropdownLink>
</DropdownMenu> </DropdownMenu>
</div>
</nav> </nav>
{/* Use primary-foreground for link */}
<Link <Link
href="/services" href="/services"
className="flex items-center text-sm font-medium text-dark hover:text-opacity-80 transition-opacity group" className="flex items-center text-sm font-medium text-primary-foreground hover:text-opacity-80 transition-opacity group"
> >
Explore Our Offerings Explore Our Offerings
<FiArrowRight className="w-4 h-4 ml-1.5 transition-transform duration-200 group-hover:translate-x-1" /> <FiArrowRight className="w-4 h-4 ml-1.5 transition-transform duration-200 group-hover:translate-x-1" />
@ -200,105 +221,74 @@ const Header = () => {
{/* Mobile Menu Panel */} {/* Mobile Menu Panel */}
<div <div
id="mobile-menu" id="mobile-menu"
className={`md:hidden absolute top-full left-0 w-full shadow-lg transition-all duration-300 ease-in-out overflow-hidden bg-light dark:bg-[var(--color-dark-bg-secondary)] border-t border-gray-200 dark:border-[var(--color-dark-border)] ${ // Use semantic variables for background and border
isMenuOpen ? "max-h-screen py-4" : "max-h-0 py-0" 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"
<nav className="container mx-auto px-4 sm:px-6 lg:px-8 flex flex-col space-y-3"> : "max-h-0 py-0" // Animate height, allow scroll
<Link
href="/"
onClick={handleMobileLinkClick}
className={`block py-2 text-base font-medium ${
isActive("/")
? "text-primary font-semibold"
: "text-gray-700 dark:text-[var(--color-dark-text)] hover:text-primary"
}`} }`}
> >
{/* 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 */}
{/* Simplified mobile links, using DropdownLink component for consistency */}
<DropdownLink href="/" onClick={handleMobileLinkClick}>
Home Home
</Link> </DropdownLink>
<Link <DropdownLink href="/about" onClick={handleMobileLinkClick}>
href="/about"
onClick={handleMobileLinkClick}
className={`block py-2 text-base font-medium ${
pathname.startsWith("/about")
? "text-primary font-semibold"
: "text-gray-700 dark:text-[var(--color-dark-text)] hover:text-primary"
}`}
>
About Us About Us
</Link> </DropdownLink>
{/* Maybe use collapsible sections here in a real app, but simple list for now */}
<span className="pt-2 text-xs uppercase text-gray-500 dark:text-gray-400"> <span className="pt-3 pb-1 text-xs uppercase text-muted-foreground">
Services Services
</span> </span>
<Link <DropdownLink
href="/services/resource-augmentation" href="/services/resource-augmentation"
onClick={handleMobileLinkClick} onClick={handleMobileLinkClick}
className="block py-1 pl-3 text-base font-medium text-gray-700 dark:text-[var(--color-dark-text)] hover:text-primary"
> >
Resource Augmentation Resource Augmentation
</Link> </DropdownLink>
<Link <DropdownLink
href="/services/project-management" href="/services/project-management"
onClick={handleMobileLinkClick} onClick={handleMobileLinkClick}
className="block py-1 pl-3 text-base font-medium text-gray-700 dark:text-[var(--color-dark-text)] hover:text-primary"
> >
Project Management Project Management
</Link> </DropdownLink>
<Link <DropdownLink
href="/services/product-development" href="/services/product-development"
onClick={handleMobileLinkClick} onClick={handleMobileLinkClick}
className="block py-1 pl-3 text-base font-medium text-gray-700 dark:text-[var(--color-dark-text)] hover:text-primary"
> >
Product Development Product Development
</Link> </DropdownLink>
<span className="pt-3 pb-1 text-xs uppercase text-muted-foreground">
<span className="pt-2 text-xs uppercase text-gray-500 dark:text-gray-400">
Products Products
</span> </span>
<Link <DropdownLink href="/products/obse" onClick={handleMobileLinkClick}>
href="/products/obse"
onClick={handleMobileLinkClick}
className="block py-1 pl-3 text-base font-medium text-gray-700 dark:text-[var(--color-dark-text)] hover:text-primary"
>
OBSE OBSE
</Link> </DropdownLink>
<span className="pt-3 pb-1 text-xs uppercase text-muted-foreground">
<span className="pt-2 text-xs uppercase text-gray-500 dark:text-gray-400">
Join Us Join Us
</span> </span>
<Link <DropdownLink
href="/join-us/vacancies" href="/join-us/vacancies"
onClick={handleMobileLinkClick} onClick={handleMobileLinkClick}
className="block py-1 pl-3 text-base font-medium text-gray-700 dark:text-[var(--color-dark-text)] hover:text-primary"
> >
Vacancies Vacancies
</Link> </DropdownLink>
<Link <DropdownLink href="/join-us/portal" onClick={handleMobileLinkClick}>
href="/join-us/portal"
onClick={handleMobileLinkClick}
className="block py-1 pl-3 text-base font-medium text-gray-700 dark:text-[var(--color-dark-text)] hover:text-primary"
>
Recruitment Portal Recruitment Portal
</Link> </DropdownLink>
<DropdownLink href="/contact" onClick={handleMobileLinkClick}>
<Link
href="/contact"
onClick={handleMobileLinkClick}
className={`block py-2 text-base font-medium ${
isActive("/contact")
? "text-primary font-semibold"
: "text-gray-700 dark:text-[var(--color-dark-text)] hover:text-primary"
}`}
>
Contact Us Contact Us
</Link> </DropdownLink>
{/* Demo button at the bottom */}
<div className="pt-4"> <div className="pt-4">
<Link <Link
href="/request-demo" href="/request-demo"
onClick={handleMobileLinkClick} onClick={handleMobileLinkClick}
className="flex w-full justify-center items-center text-sm font-medium bg-primary text-dark px-4 py-2 rounded-md hover:bg-opacity-90 transition-colors" // 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" title="Request a Demo"
> >
<FiClipboard className="w-4 h-4 mr-1.5" /> <FiClipboard className="w-4 h-4 mr-1.5" />

View File

@ -4,7 +4,7 @@ import Link from "next/link";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
href?: string; href?: string;
variant?: "primary" | "secondary" | "outline"; variant?: "primary" | "secondary" | "outline" | "ghost" | "destructive"; // Added more variants
size?: "sm" | "md" | "lg"; size?: "sm" | "md" | "lg";
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
@ -18,20 +18,25 @@ const Button: React.FC<ButtonProps> = ({
className = "", className = "",
...props ...props
}) => { }) => {
// Base styles including focus ring using semantic vars
const baseStyle = const baseStyle =
"inline-flex items-center justify-center rounded-md font-semibold transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2"; "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";
// Variant styles using semantic vars (Tailwind classes generated via @theme)
const variantStyles = { const variantStyles = {
primary: "bg-primary text-dark hover:bg-primary/90 focus:ring-primary/50", primary: "bg-primary text-primary-foreground hover:bg-primary/90", // bg-primary generated from --primary
secondary: "bg-dark text-light hover:bg-gray-800 focus:ring-dark/50", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline: outline:
"border border-primary text-primary hover:bg-primary hover:text-dark focus:ring-primary/50", "border border-border bg-transparent hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
}; };
const sizeStyles = { const sizeStyles = {
sm: "px-4 py-2 text-sm", sm: "px-3 py-1.5 text-sm",
md: "px-6 py-3 text-base", md: "px-4 py-2 text-base",
lg: "px-8 py-4 text-lg", lg: "px-6 py-3 text-lg",
}; };
const combinedClassName = `${baseStyle} ${variantStyles[variant]} ${sizeStyles[size]} ${className}`; const combinedClassName = `${baseStyle} ${variantStyles[variant]} ${sizeStyles[size]} ${className}`;
@ -45,7 +50,7 @@ const Button: React.FC<ButtonProps> = ({
} }
return ( return (
<button className={combinedClassName} {...props}> <button type="button" className={combinedClassName} {...props}>
{children} {children}
</button> </button>
); );

286
package-lock.json generated
View File

@ -9,10 +9,12 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"next": "15.3.1", "next": "15.3.1",
"next-auth": "^5.0.0-beta.26",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-spinners": "^0.16.1",
"react-toggle-dark-mode": "^1.1.1" "react-toggle-dark-mode": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
@ -54,6 +56,35 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@auth/core": {
"version": "0.39.0",
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.39.0.tgz",
"integrity": "sha512-jusviw/sUSfAh6S/wjY5tRmJOq0Itd3ImF+c/b4HB9DfmfChtcfVJTNJeqCeExeCG8oh4PBKRsMQJsn2W6NhFQ==",
"license": "ISC",
"dependencies": {
"@panva/hkdf": "^1.2.1",
"jose": "^6.0.6",
"oauth4webapi": "^3.3.0",
"preact": "10.24.3",
"preact-render-to-string": "6.5.11"
},
"peerDependencies": {
"@simplewebauthn/browser": "^9.0.1",
"@simplewebauthn/server": "^9.0.2",
"nodemailer": "^6.8.0"
},
"peerDependenciesMeta": {
"@simplewebauthn/browser": {
"optional": true
},
"@simplewebauthn/server": {
"optional": true
},
"nodemailer": {
"optional": true
}
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.26.2", "version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
@ -499,6 +530,13 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/runtime/node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT",
"peer": true
},
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.27.0", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
@ -1702,6 +1740,15 @@
"node": ">=12.4.0" "node": ">=12.4.0"
} }
}, },
"node_modules/@panva/hkdf": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/@react-native/assets-registry": { "node_modules/@react-native/assets-registry": {
"version": "0.79.1", "version": "0.79.1",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.1.tgz", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.1.tgz",
@ -1950,64 +1997,6 @@
"integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@react-three/fiber": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.1.2.tgz",
"integrity": "sha512-k8FR9yVHV9kIF3iuOD0ds5hVymXYXfgdKklqziBVod9ZEJ8uk05Zjw29J/omU3IKeUfLNAIHfxneN3TUYM4I2w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/react-reconciler": "^0.28.9",
"@types/webxr": "*",
"base64-js": "^1.5.1",
"buffer": "^6.0.3",
"its-fine": "^2.0.0",
"react-reconciler": "^0.31.0",
"react-use-measure": "^2.1.7",
"scheduler": "^0.25.0",
"suspend-react": "^0.1.3",
"use-sync-external-store": "^1.4.0",
"zustand": "^5.0.3"
},
"peerDependencies": {
"expo": ">=43.0",
"expo-asset": ">=8.4",
"expo-file-system": ">=11.0",
"expo-gl": ">=11.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-native": ">=0.78",
"three": ">=0.156"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
},
"expo-asset": {
"optional": true
},
"expo-file-system": {
"optional": true
},
"expo-gl": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/@react-three/fiber/node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT",
"peer": true
},
"node_modules/@rtsao/scc": { "node_modules/@rtsao/scc": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@ -6118,16 +6107,16 @@
} }
}, },
"node_modules/its-fine": { "node_modules/its-fine": {
"version": "2.0.0", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz",
"integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@types/react-reconciler": "^0.28.9" "@types/react-reconciler": "^0.28.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^19.0.0" "react": ">=18.0"
} }
}, },
"node_modules/jest-environment-node": { "node_modules/jest-environment-node": {
@ -6321,6 +6310,15 @@
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
} }
}, },
"node_modules/jose": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.0.10.tgz",
"integrity": "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -7368,6 +7366,33 @@
} }
} }
}, },
"node_modules/next-auth": {
"version": "5.0.0-beta.26",
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.26.tgz",
"integrity": "sha512-yAQLIP2x6FAM+GX6FTlQjoPph6msO/9HI3pjI1z1yws3VnvS77atetcxQOmCpxSLTO4jzvpQqPaBZMgRxDgsYg==",
"license": "ISC",
"dependencies": {
"@auth/core": "0.39.0"
},
"peerDependencies": {
"@simplewebauthn/browser": "^9.0.1",
"@simplewebauthn/server": "^9.0.2",
"next": "^14.0.0-0 || ^15.0.0-0",
"nodemailer": "^6.6.5",
"react": "^18.2.0 || ^19.0.0-0"
},
"peerDependenciesMeta": {
"@simplewebauthn/browser": {
"optional": true
},
"@simplewebauthn/server": {
"optional": true
},
"nodemailer": {
"optional": true
}
}
},
"node_modules/next-themes": { "node_modules/next-themes": {
"version": "0.4.6", "version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
@ -7437,6 +7462,15 @@
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/oauth4webapi": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.5.0.tgz",
"integrity": "sha512-DF3mLWNuxPkxJkHmWxbSFz4aE5CjWOsm465VBfBdWzmzX4Mg3vF8icxK+iKqfdWrIumBJ2TaoNQWx+SQc2bsPQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/ob1": { "node_modules/ob1": {
"version": "0.82.1", "version": "0.82.1",
"resolved": "https://registry.npmjs.org/ob1/-/ob1-0.82.1.tgz", "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.82.1.tgz",
@ -7831,6 +7865,25 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/preact": {
"version": "10.24.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/preact-render-to-string": {
"version": "6.5.11",
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz",
"integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==",
"license": "MIT",
"peerDependencies": {
"preact": ">=10"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -8051,19 +8104,6 @@
"react-dom": ">=18.0.0" "react-dom": ">=18.0.0"
} }
}, },
"node_modules/react-konva/node_modules/its-fine": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz",
"integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/react-reconciler": "^0.28.0"
},
"peerDependencies": {
"react": ">=18.0"
}
},
"node_modules/react-konva/node_modules/react-reconciler": { "node_modules/react-konva/node_modules/react-reconciler": {
"version": "0.29.2", "version": "0.29.2",
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz",
@ -8151,13 +8191,6 @@
} }
} }
}, },
"node_modules/react-native/node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"peer": true
},
"node_modules/react-native/node_modules/scheduler": { "node_modules/react-native/node_modules/scheduler": {
"version": "0.25.0", "version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
@ -8198,6 +8231,16 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-spinners": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.16.1.tgz",
"integrity": "sha512-hYDQp2mmmv3a3JZZZYOi3+jW7C/ro51Ny71TfkRXhoPBb6wZuK9BgdvYbTnSAUrQCrVnOLSpWfZatncUTb6n5w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-toggle-dark-mode": { "node_modules/react-toggle-dark-mode": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/react-toggle-dark-mode/-/react-toggle-dark-mode-1.1.1.tgz", "resolved": "https://registry.npmjs.org/react-toggle-dark-mode/-/react-toggle-dark-mode-1.1.1.tgz",
@ -8213,6 +8256,19 @@
"react": ">=16" "react": ">=16"
} }
}, },
"node_modules/react-toggle-dark-mode/node_modules/its-fine": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz",
"integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/react-reconciler": "^0.28.9"
},
"peerDependencies": {
"react": "^19.0.0"
}
},
"node_modules/react-toggle-dark-mode/node_modules/react-dom": { "node_modules/react-toggle-dark-mode/node_modules/react-dom": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@ -8461,6 +8517,64 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/react-toggle-dark-mode/node_modules/react-spring/node_modules/@react-three/fiber": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.1.2.tgz",
"integrity": "sha512-k8FR9yVHV9kIF3iuOD0ds5hVymXYXfgdKklqziBVod9ZEJ8uk05Zjw29J/omU3IKeUfLNAIHfxneN3TUYM4I2w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.17.8",
"@types/react-reconciler": "^0.28.9",
"@types/webxr": "*",
"base64-js": "^1.5.1",
"buffer": "^6.0.3",
"its-fine": "^2.0.0",
"react-reconciler": "^0.31.0",
"react-use-measure": "^2.1.7",
"scheduler": "^0.25.0",
"suspend-react": "^0.1.3",
"use-sync-external-store": "^1.4.0",
"zustand": "^5.0.3"
},
"peerDependencies": {
"expo": ">=43.0",
"expo-asset": ">=8.4",
"expo-file-system": ">=11.0",
"expo-gl": ">=11.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-native": ">=0.78",
"three": ">=0.156"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
},
"expo-asset": {
"optional": true
},
"expo-file-system": {
"optional": true
},
"expo-gl": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-toggle-dark-mode/node_modules/react-spring/node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT",
"peer": true
},
"node_modules/react-toggle-dark-mode/node_modules/scheduler": { "node_modules/react-toggle-dark-mode/node_modules/scheduler": {
"version": "0.23.2", "version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@ -8560,9 +8674,9 @@
} }
}, },
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.14.1", "version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },

View File

@ -10,10 +10,12 @@
}, },
"dependencies": { "dependencies": {
"next": "15.3.1", "next": "15.3.1",
"next-auth": "^5.0.0-beta.26",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-spinners": "^0.16.1",
"react-toggle-dark-mode": "^1.1.1" "react-toggle-dark-mode": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {

BIN
public/obse-mockup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

BIN
public/oms-logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

80
utils/jsonLdSchema.ts Normal file
View File

@ -0,0 +1,80 @@
function JsonLdSchema() {
const schema = {
'@context': 'https://schema.org',
'@type': 'Organization', // Or 'LocalBusiness' if more appropriate
name: 'Owethu Managed Services (OMS)',
alternateName: 'OMS',
url: 'https://www.oms.africa', // Replace with your actual domain
logo: 'https://www.oms.africa/oms-logo.svg', // Replace with absolute URL to your logo
contactPoint: {
'@type': 'ContactPoint',
telephone: '+27-12-051-3282', // Use international format
contactType: 'Customer Service', // Or 'Sales', 'Technical Support'
areaServed: 'ZA', // ISO 3166-1 alpha-2 code for South Africa
availableLanguage: ['en'],
},
address: { // Add address if applicable, especially for LocalBusiness
'@type': 'PostalAddress',
streetAddress': 'Unit 10 B Centuria Park, 265 Von Willich Avenue',
addressLocality': 'Centurion',
addressRegion': 'GP', // Gauteng province code
postalCode': '0159',
addressCountry': 'ZA'
},
sameAs: [ // Links to social media profiles
'https://www.linkedin.com/your-linkedin-profile', // Replace with actual URLs
'https://www.instagram.com/your-instagram-profile',
// Add other relevant profiles (Twitter, Facebook, etc.)
],
description: metadata.description, // Reuse description from metadata
// Define key services offered
makesOffer: [
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "IT Resource Augmentation",
"description": "Providing top-tier IT talent and skilled professionals for short-term or long-term project needs."
// "url": "https://www.oms.africa/services/resource-augmentation" // Add specific service URL if available
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "IT Project Management",
"description": "Expert project management ensuring on-time, within-budget delivery with superior results."
// "url": "https://www.oms.africa/services/project-management"
}
},
{
"@type": "Offer",
"itemOffered": {
"@type": "Service",
"name": "Custom Software & Product Development",
"description": "Creating innovative, scalable digital products and custom software solutions."
// "url": "https://www.oms.africa/services/product-development"
}
},
{
"@type": "Offer",
"itemOffered": {
// Use SoftwareApplication or Product depending on how you market OBSE
"@type": "SoftwareApplication",
"name": "OBSE (Optical Bank Statement Extractor)",
"description": "Advanced tool for automating bank statement OCR and financial data aggregation.",
// "url": "https://www.oms.africa/products/obse"
"applicationCategory": "FinanceApplication",
"operatingSystem": "WebPlatform" // Or specify OS if applicable
}
}
]
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}