mirror of
https://github.com/OwethuManagedServices/oms-website-nextjs.git
synced 2025-12-17 17:18:09 +00:00
Home page completed
This commit is contained in:
43
app/(website)/_components/CallToActionSection.tsx
Normal file
43
app/(website)/_components/CallToActionSection.tsx
Normal 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;
|
||||||
84
app/(website)/_components/ClientLogosSection.tsx
Normal file
84
app/(website)/_components/ClientLogosSection.tsx
Normal 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;
|
||||||
76
app/(website)/_components/CoreServicesSection.tsx
Normal file
76
app/(website)/_components/CoreServicesSection.tsx
Normal 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;
|
||||||
108
app/(website)/_components/FeaturedProductSection.tsx
Normal file
108
app/(website)/_components/FeaturedProductSection.tsx
Normal 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;
|
||||||
67
app/(website)/_components/HeroSection.tsx
Normal file
67
app/(website)/_components/HeroSection.tsx
Normal 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;
|
||||||
98
app/(website)/_components/HeroSectionDynamic.tsx
Normal file
98
app/(website)/_components/HeroSectionDynamic.tsx
Normal 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;
|
||||||
80
app/(website)/_components/HeroSectionModern.tsx
Normal file
80
app/(website)/_components/HeroSectionModern.tsx
Normal 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;
|
||||||
27
app/(website)/_components/ServiceCard.tsx
Normal file
27
app/(website)/_components/ServiceCard.tsx
Normal 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;
|
||||||
97
app/(website)/_components/WhyChooseUsSection.tsx
Normal file
97
app/(website)/_components/WhyChooseUsSection.tsx
Normal 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
94
app/(website)/page.tsx
Normal 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"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
2
app/api/auth/[...nextauth]/route.ts
Normal file
2
app/api/auth/[...nextauth]/route.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { handlers } from "@/auth"; // Referring to the auth.ts we just created
|
||||||
|
export const { GET, POST } = handlers;
|
||||||
@ -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 */
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
12
app/loading.tsx
Normal 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;
|
||||||
168
app/page.tsx
168
app/page.tsx
@ -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'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
6
auth.ts
Normal 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],
|
||||||
|
});
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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,34 +168,48 @@ 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 */}
|
||||||
<DropdownMenu trigger={<span>Services</span>}>
|
<nav className="flex items-center space-x-6 lg:space-x-8 text-primary-foreground">
|
||||||
<DropdownLink href="/services/resource-augmentation">
|
{/* Wrap dropdown triggers for consistent hover */}
|
||||||
Resource Augmentation
|
<div className="hover:text-opacity-80 transition-opacity">
|
||||||
</DropdownLink>
|
<DropdownMenu trigger={<span>Services</span>}>
|
||||||
<DropdownLink href="/services/project-management">
|
<DropdownLink href="/services/resource-augmentation">
|
||||||
Project Management
|
Resource Augmentation
|
||||||
</DropdownLink>
|
</DropdownLink>
|
||||||
<DropdownLink href="/services/product-development">
|
<DropdownLink href="/services/project-management">
|
||||||
Product Development
|
Project Management
|
||||||
</DropdownLink>
|
</DropdownLink>
|
||||||
</DropdownMenu>
|
<DropdownLink href="/services/product-development">
|
||||||
<DropdownMenu trigger={<span>Products</span>}>
|
Product Development
|
||||||
<DropdownLink href="/products/obse">OBSE</DropdownLink>
|
</DropdownLink>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<DropdownMenu trigger={<span>Join Our Team</span>}>
|
</div>
|
||||||
<DropdownLink href="/join-us/vacancies">Vacancies</DropdownLink>
|
<div className="hover:text-opacity-80 transition-opacity">
|
||||||
<DropdownLink href="/join-us/portal">
|
<DropdownMenu trigger={<span>Products</span>}>
|
||||||
Recruitment Portal
|
<DropdownLink href="/products/obse">OBSE</DropdownLink>
|
||||||
</DropdownLink>
|
</DropdownMenu>
|
||||||
</DropdownMenu>
|
</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>
|
||||||
|
</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"
|
||||||
|
: "max-h-0 py-0" // Animate height, allow scroll
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<nav className="container mx-auto px-4 sm:px-6 lg:px-8 flex flex-col space-y-3">
|
{/* Use semantic variable for text color */}
|
||||||
<Link
|
<nav className="container mx-auto px-4 sm:px-6 lg:px-8 flex flex-col space-y-1 text-foreground">
|
||||||
href="/"
|
{" "}
|
||||||
onClick={handleMobileLinkClick}
|
{/* Reduced space-y */}
|
||||||
className={`block py-2 text-base font-medium ${
|
{/* Simplified mobile links, using DropdownLink component for consistency */}
|
||||||
isActive("/")
|
<DropdownLink href="/" onClick={handleMobileLinkClick}>
|
||||||
? "text-primary font-semibold"
|
|
||||||
: "text-gray-700 dark:text-[var(--color-dark-text)] hover:text-primary"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
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" />
|
||||||
|
|||||||
@ -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
286
package-lock.json
generated
@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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
BIN
public/obse-mockup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 199 KiB |
BIN
public/oms-logo.jpg
Normal file
BIN
public/oms-logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 445 KiB |
80
utils/jsonLdSchema.ts
Normal file
80
utils/jsonLdSchema.ts
Normal 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) }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user