8 Commits

12 changed files with 201 additions and 51 deletions

View File

@ -1,42 +1,85 @@
// components/ClientLogosSection.tsx "use client";
import React from "react";
import { useRef, useEffect, useState } from "react";
import Image from "next/image"; import Image from "next/image";
// Define structure for client data - focusing on logos now
type Client = { type Client = {
name: string; name: string;
logoUrl: string; // Expecting actual logo URLs now logoUrl: string;
}; };
type ClientLogosSectionProps = { type ClientLogosSectionProps = {
title: string; title: string;
description?: string; description?: string;
clients: Client[]; clients: Client[];
/** Control animation speed (lower number = faster). Default: 40s */ speed?: number; // pixels per frame
speed?: number;
/** Size of the square background container in pixels. Default: 120 */
squareSize?: number; squareSize?: number;
}; };
const ClientLogosSection: React.FC<ClientLogosSectionProps> = ({ const ClientLogosSection = ({
title, title,
description, description,
clients = [], // Default to empty array clients = [],
speed = 40, //Default speed in seconds for one full cycle speed = 1,
squareSize = 120, // Default size for the square container (e.g., 120px) squareSize = 120,
}) => { }: ClientLogosSectionProps) => {
// Need at least one client to render the marquee if (clients.length === 0) return null;
if (clients.length === 0) {
return null; // Or render a placeholder message if preferred
}
// Duplicate the clients for a seamless loop effect
const extendedClients = [...clients, ...clients]; const extendedClients = [...clients, ...clients];
const squareDim = `${squareSize}px`;
const squareDim = `${squareSize}px`; // Convert size to string for inline style const padding = Math.round(squareSize / 6);
const padding = Math.round(squareSize / 6); // Calculate padding based on size (adjust divisor as needed)
const paddingDim = `${padding}px`; const paddingDim = `${padding}px`;
const scrollRef = useRef<HTMLDivElement>(null);
const [direction, setDirection] = useState<"left" | "right">("left");
const [paused, setPaused] = useState(false);
let resumeTimeout: NodeJS.Timeout;
const pauseAndScroll = (dir: "left" | "right") => {
if (!scrollRef.current) return;
setPaused(true);
// Scroll manually
scrollRef.current.scrollBy({
left: dir === "left" ? -200 : 200,
behavior: "smooth",
});
// Clear previous timeout and resume after 3 seconds
clearTimeout(resumeTimeout);
resumeTimeout = setTimeout(() => {
setPaused(false);
}, 3000);
// Set the direction for automatic scroll after pause
setDirection(dir === "left" ? "right" : "left");
};
useEffect(() => {
const container = scrollRef.current;
if (!container) return;
let animationFrame: number;
const step = () => {
if (!paused) {
if (direction === "left") {
container.scrollLeft += speed;
if (container.scrollLeft >= container.scrollWidth / 2) container.scrollLeft = 0;
} else {
container.scrollLeft -= speed;
if (container.scrollLeft <= 0) container.scrollLeft = container.scrollWidth / 2;
}
}
animationFrame = requestAnimationFrame(step);
};
animationFrame = requestAnimationFrame(step);
return () => cancelAnimationFrame(animationFrame);
}, [direction, paused, speed]);
return ( return (
<section className="py-16 md:py-20 bg-background overflow-hidden"> <section className="py-16 md:py-20 bg-background overflow-hidden">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center"> <div className="container mx-auto px-4 sm:px-6 lg:px-8 text-center">
@ -45,19 +88,17 @@ const ClientLogosSection: React.FC<ClientLogosSectionProps> = ({
</h2> </h2>
</div> </div>
{/* Marquee container - group allows pausing animation on hover */} {/* Logos Container */}
<div className="relative w-full overflow-hidden group"> <div className="relative w-full overflow-hidden">
{/* Inner container that will animate */}
<div <div
className="flex flex-nowrap animate-marquee-continuous" ref={scrollRef}
style={{ animationDuration: `${speed}s` }} className="flex flex-nowrap overflow-x-hidden scrollbar-hide"
> >
{extendedClients.map((client, index) => ( {extendedClients.map((client, index) => (
<div <div
key={`${client.name}-${index}`} key={`${client.name}-${index}`}
className="flex-shrink-0 mx-12 md:mx-16 py-4" className="flex-shrink-0 mx-12 md:mx-16 py-4"
> >
{/* Square Background Container */}
<div <div
title={client.name} title={client.name}
className=" className="
@ -73,23 +114,35 @@ const ClientLogosSection: React.FC<ClientLogosSectionProps> = ({
style={{ style={{
width: squareDim, width: squareDim,
height: squareDim, height: squareDim,
padding: paddingDim, // Add padding inside the square padding: paddingDim,
}} }}
> >
<Image <Image
src={client.logoUrl} src={client.logoUrl}
alt={`${client.name} Logo`} alt={`${client.name} Logo`}
layout="fill" // Let image fill the relative container fill
objectFit="contain" // Maintain aspect ratio within the container style={{ objectFit: "contain" }}
/> />
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div>
{/* Optional: Add fade effect at the edges */} {/* Arrow Controls */}
<div className="absolute inset-y-0 left-0 w-16 md:w-24 bg-gradient-to-r from-background to-transparent pointer-events-none z-10"></div> <div className="flex justify-center mt-8 space-x-6">
<div className="absolute inset-y-0 right-0 w-16 md:w-24 bg-gradient-to-l from-background to-transparent pointer-events-none z-10"></div> <button
onClick={() => pauseAndScroll("right")}
className="px-4 py-2 rounded-full bg-muted hover:bg-muted/70 transition"
>
</button>
<button
onClick={() => pauseAndScroll("left")}
className="px-4 py-2 rounded-full bg-muted hover:bg-muted/70 transition"
>
</button>
</div> </div>
{description && ( {description && (

View File

@ -15,6 +15,8 @@ import {
FaUserCheck, FaUserCheck,
FaProjectDiagram, FaProjectDiagram,
} from "react-icons/fa"; } from "react-icons/fa";
import { Metadata } from "next";
// const leadershipTeam = [ // const leadershipTeam = [
// { // {
@ -42,6 +44,23 @@ import {
// linkedinUrl: "#", // linkedinUrl: "#",
// }, // },
// ]; // ];
export const metadata: Metadata = {
title: "About Us | Owethu Managed Services (OMS)",
description: "Learn about OMS, our mission, vision, and the values that drive us to deliver exceptional IT solutions and services.",
keywords: [
"Owethu Managed Services",
"About OMS",
"OMS",
"Black-owned ",
"Women-owned",
"Tech company",
"bank statement reader",
"fintech solutions" ,
],
}
const coreValues = [ const coreValues = [
{ {

View File

@ -12,6 +12,7 @@ import {
import { COLORS } from "@/constants"; // Using COLORS constant import { COLORS } from "@/constants"; // Using COLORS constant
import ContactForm from "@/components/ContactForm"; import ContactForm from "@/components/ContactForm";
// Define the structure for FAQ items // Define the structure for FAQ items
interface FAQItem { interface FAQItem {
id: number; id: number;

View File

@ -29,6 +29,7 @@ export const metadata: Metadata = {
"fintech solutions", "fintech solutions",
"IT consulting", "IT consulting",
"OMS", "OMS",
"CVEvolve",
"Owethu Managed Services", "Owethu Managed Services",
"Centurion", "Centurion",
"Gauteng", "Gauteng",

View File

@ -14,6 +14,7 @@ import FeaturedProductSection, {
} from "./_components/FeaturedProductSection"; } from "./_components/FeaturedProductSection";
import { getHome } from "@/lib/query/home"; import { getHome } from "@/lib/query/home";
export default async function HomePage() { export default async function HomePage() {
// Explicitly type the data variable, assuming getHome returns HeroSectionType or null/undefined // Explicitly type the data variable, assuming getHome returns HeroSectionType or null/undefined
const data = await getHome(); const data = await getHome();

View File

@ -8,6 +8,7 @@ export const metadata: Metadata = {
description: description:
"Our recruitment portal is currently under development. Stay tuned for updates on career opportunities at Owethu Managed Services.", "Our recruitment portal is currently under development. Stay tuned for updates on career opportunities at Owethu Managed Services.",
robots: "noindex, nofollow", // Prevent indexing of the coming soon page robots: "noindex, nofollow", // Prevent indexing of the coming soon page
}; };
export default function RecruitmentPortalPage() { export default function RecruitmentPortalPage() {

View File

@ -25,6 +25,8 @@ export const metadata: Metadata = {
"IT resource augmentation", "IT resource augmentation",
"managed IT services", "managed IT services",
"OBSE", "OBSE",
"CVEvolve",
"bank statement extractor",
"bank statement automation", "bank statement automation",
"fintech solutions", "fintech solutions",
"IT consulting", "IT consulting",

View File

@ -11,6 +11,7 @@ export const metadata: Metadata = {
alternates: { alternates: {
canonical: "/tech-talk", canonical: "/tech-talk",
}, },
openGraph: { openGraph: {
title: "OMS TechTalk | Insights & Innovation", title: "OMS TechTalk | Insights & Innovation",
description: "Stay updated with tech insights from OMS.", description: "Stay updated with tech insights from OMS.",

View File

@ -208,18 +208,16 @@
} /* Added longer delay */ } /* Added longer delay */
} }
@keyframes fadeInUp {
from { @keyframes marquee {
opacity: 0; from { transform: translateX(0); }
transform: translateY(20px); to { transform: translateX(-50%); }
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.animate-fade-in-up { .animate-marquee-continuous {
animation: fadeInUp 0.8s ease-out forwards; animation: marquee linear infinite;
opacity: 0; /* Start hidden */ }
.paused {
animation-play-state: paused !important;
} }

View File

@ -4,6 +4,7 @@ import "./globals.css";
import Header from "@/components/Header"; import Header from "@/components/Header";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import { ThemeProvider } from "@/providers/theme-provider"; import { ThemeProvider } from "@/providers/theme-provider";
import Script from "next/script";
const poppins = Poppins({ const poppins = Poppins({
subsets: ["latin"], subsets: ["latin"],
@ -19,6 +20,7 @@ export const metadata: Metadata = {
"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 "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: [ keywords: [
"Owethu Managed Services", "Owethu Managed Services",
"OMS",
"OBSE", "OBSE",
"IT solutions South Africa", "IT solutions South Africa",
"resource augmentation", "resource augmentation",
@ -27,6 +29,7 @@ export const metadata: Metadata = {
"OBSE", "OBSE",
"financial data analysis", "financial data analysis",
"IT services Centurion", "IT services Centurion",
"digital transformation",
], // Add relevant keywords ], // Add relevant keywords
alternates: { alternates: {
canonical: "/", // Assuming this is the root URL canonical: "/", // Assuming this is the root URL
@ -95,6 +98,14 @@ export default function RootLayout({
<main>{children}</main> <main>{children}</main>
<Footer /> <Footer />
</ThemeProvider> </ThemeProvider>
<Script
src="https://umami.obse.africa/script.js"
data-website-id={
process.env.NEXT_PUBLIC_UMAMI_WEB_ID ||
"d9f0e4d7-0f0a-45e4-91bf-62e0e65e25d2"
}
strategy="afterInteractive"
/>
</body> </body>
</html> </html>
); );

View File

@ -1,9 +1,10 @@
// 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"; import { FaLinkedin, FaInstagram, FaYoutube } from "react-icons/fa";
import Image from "next/image"; import Image from "next/image";
const omsLogoUrl = "/oms-logo.svg"; // Ensure this exists in /public const omsLogoUrl = "/oms-logo.svg"; // Ensure this exists in /public
// const omsLogoDarkUrl = "/oms-logo-dark.svg"; // Optional dark mode logo // const omsLogoDarkUrl = "/oms-logo-dark.svg"; // Optional dark mode logo
@ -127,7 +128,7 @@ const Footer = () => {
{/* Social Icons - Use muted bright color, primary on hover */} {/* 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://www.linkedin.com/company/owethu-managed-services-oms"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-[var(--oms-white)]/70 hover:text-primary transition-colors" className="text-[var(--oms-white)]/70 hover:text-primary transition-colors"
@ -135,8 +136,9 @@ const Footer = () => {
> >
<FaLinkedin size={24} /> <FaLinkedin size={24} />
</a> </a>
<a <a
href="https://instagram.com" href="https://www.instagram.com/owethumanagedservices_oms"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-[var(--oms-white)]/70 hover:text-primary transition-colors" className="text-[var(--oms-white)]/70 hover:text-primary transition-colors"
@ -144,6 +146,17 @@ const Footer = () => {
> >
<FaInstagram size={24} /> <FaInstagram size={24} />
</a> </a>
<a
href="https://www.youtube.com/@OwethuManagedServices-africa"
target="_blank"
rel="noopener noreferrer"
className="text-[var(--oms-white)]/70 hover:text-primary transition-colors"
aria-label="YouTube"
>
<FaYoutube size={24} />
</a>
</div> </div>
</div> </div>
@ -223,6 +236,8 @@ const Footer = () => {
> >
Privacy Policy Privacy Policy
</Link> */} </Link> */}
<a <a
href="/Privacy-Policy/Recruitment-Privacy-Policy.pdf" href="/Privacy-Policy/Recruitment-Privacy-Policy.pdf"
className="hover:text-primary transition-colors" className="hover:text-primary transition-colors"
@ -236,6 +251,7 @@ const Footer = () => {
PAIA & POPIA PAIA & POPIA
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</footer> </footer>

View File

@ -13,6 +13,7 @@ import {
FiUsers, FiUsers,
FiCpu, FiCpu,
FiBox, FiBox,
FiLayers
} from "react-icons/fi"; } from "react-icons/fi";
import ThemeToggle from "./ThemeToggle"; import ThemeToggle from "./ThemeToggle";
@ -265,15 +266,16 @@ const HeaderClient = () => {
</div> </div>
</div> </div>
</div> </div>
{/* Products */} {/* Products */}
<div <div
className={`group ${isActive("/obse") ? "active" : ""}`} // Add active class to group className={`group ${isActive("/products") ? "active" : ""}`} // Add active class to group
onMouseEnter={handleProductsMouseEnter} onMouseEnter={handleProductsMouseEnter}
onMouseLeave={handleProductsMouseLeave} onMouseLeave={handleProductsMouseLeave}
> >
<button <button
className={`${megaMenuTriggerClasses} ${ className={`${megaMenuTriggerClasses} ${
isActive("/obse") ? "after:w-full" : "" isActive("/products") ? "after:w-full" : ""
}`} }`}
> >
{" "} {" "}
@ -283,7 +285,7 @@ const HeaderClient = () => {
</button> </button>
<div <div
className={` className={`
absolute left-0 top-full w-full shadow-lg z-1000 absolute left-0 top-full w-full shadow-lg z-40
bg-card border-x border-b border-border rounded-b-lg bg-card border-x border-b border-border rounded-b-lg
opacity-0 invisible translate-y-[-10px] opacity-0 invisible translate-y-[-10px]
${ ${
@ -295,7 +297,7 @@ const HeaderClient = () => {
`} `}
> >
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-5"> <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-5">
<div className="max-w-md"> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 max-w-xl">
<Link <Link
href="/obse" href="/obse"
className={`${megaMenuItemClasses} ${ className={`${megaMenuItemClasses} ${
@ -307,7 +309,44 @@ const HeaderClient = () => {
<FiBox className={megaMenuIconClasses} /> <FiBox className={megaMenuIconClasses} />
<div className={megaMenuTextWrapperClasses}> <div className={megaMenuTextWrapperClasses}>
<p className={megaMenuTitleClasses}> <p className={megaMenuTitleClasses}>
OBSE Platform OBSE Platform
</p>
</div>
</Link>
{/* Add more service links here
<Link
href="/services/project-management"
className={`${megaMenuItemClasses} ${
isActive("/services/project-management")
? "text-primary"
: ""
}`} // Apply active class
>
<FiBriefcase className={megaMenuIconClasses} />
<div className={megaMenuTextWrapperClasses}>
<p className={megaMenuTitleClasses}>
Project Management
</p>
</div>
</Link>
*/}
<Link
href="https://cvevolve.com/"
target="_blank"
className={`${megaMenuItemClasses} ${
isActive("/obse") ? "text-primary" : ""
}`}
>
{" "}
{/* Apply active class */}
<FiLayers className={megaMenuIconClasses} />
<div className={megaMenuTextWrapperClasses}>
<p className={megaMenuTitleClasses}>
CVEvolve
</p> </p>
</div> </div>
</Link> </Link>
@ -315,6 +354,8 @@ const HeaderClient = () => {
</div> </div>
</div> </div>
</div> </div>
{/* Join Our Team */} {/* Join Our Team */}
@ -425,6 +466,11 @@ const HeaderClient = () => {
OBSE OBSE
</DropdownLink> </DropdownLink>
{/* small screen investigation */}
<DropdownLink href="https://cvevolve.com" onClick={handleMobileLinkClick}>
CVEvolve
</DropdownLink>
{/* <span className="pt-3 pb-1 text-xs uppercase text-muted-foreground"> {/* <span className="pt-3 pb-1 text-xs uppercase text-muted-foreground">
Join Us Join Us
</span> </span>