mirror of
https://github.com/OwethuManagedServices/oms-website-nextjs.git
synced 2025-12-17 18:58:10 +00:00
Compare commits
34 Commits
25-05-2025
...
f0c68f549d
| Author | SHA1 | Date | |
|---|---|---|---|
| f0c68f549d | |||
| 2fade93bf7 | |||
| 33008c17f9 | |||
| 8e889d5e53 | |||
| 0530c9c943 | |||
| 8d40a10e02 | |||
| 985c4b5a85 | |||
| 755eea55a4 | |||
| 98f581b348 | |||
| 8f828a374c | |||
| 45822f9af8 | |||
| 3d5417852c | |||
| af917f3484 | |||
| 0658c1ce28 | |||
| 7ba4ef1872 | |||
| 2044f7207e | |||
| 61ce1848d2 | |||
| 4f71f687d6 | |||
| 91e2f34a63 | |||
| 78e74dfc7d | |||
| be8a2fe95d | |||
| 9949158d31 | |||
| 6188a7ffbc | |||
| 985f5c43ba | |||
| 25691d6a2e | |||
| 6f3c946845 | |||
| b84b287dc1 | |||
| b1f701e55d | |||
| d1c497c936 | |||
| 6d2a8c1a59 | |||
| af744bd192 | |||
| 2eb3e35cdb | |||
| b6bbf9b54d | |||
| 730dc51629 |
@ -1,42 +1,85 @@
|
||||
// components/ClientLogosSection.tsx
|
||||
import React from "react";
|
||||
"use client";
|
||||
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
// Define structure for client data - focusing on logos now
|
||||
type Client = {
|
||||
name: string;
|
||||
logoUrl: string; // Expecting actual logo URLs now
|
||||
logoUrl: string;
|
||||
};
|
||||
|
||||
type ClientLogosSectionProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
clients: Client[];
|
||||
/** Control animation speed (lower number = faster). Default: 40s */
|
||||
speed?: number;
|
||||
/** Size of the square background container in pixels. Default: 120 */
|
||||
speed?: number; // pixels per frame
|
||||
squareSize?: number;
|
||||
};
|
||||
|
||||
const ClientLogosSection: React.FC<ClientLogosSectionProps> = ({
|
||||
const ClientLogosSection = ({
|
||||
title,
|
||||
description,
|
||||
clients = [], // Default to empty array
|
||||
speed = 40, //Default speed in seconds for one full cycle
|
||||
squareSize = 120, // Default size for the square container (e.g., 120px)
|
||||
}) => {
|
||||
// Need at least one client to render the marquee
|
||||
if (clients.length === 0) {
|
||||
return null; // Or render a placeholder message if preferred
|
||||
}
|
||||
clients = [],
|
||||
speed = 1,
|
||||
squareSize = 120,
|
||||
}: ClientLogosSectionProps) => {
|
||||
if (clients.length === 0) return null;
|
||||
|
||||
// Duplicate the clients for a seamless loop effect
|
||||
const extendedClients = [...clients, ...clients];
|
||||
|
||||
const squareDim = `${squareSize}px`; // Convert size to string for inline style
|
||||
const padding = Math.round(squareSize / 6); // Calculate padding based on size (adjust divisor as needed)
|
||||
const squareDim = `${squareSize}px`;
|
||||
const padding = Math.round(squareSize / 6);
|
||||
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 (
|
||||
<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">
|
||||
@ -45,19 +88,17 @@ const ClientLogosSection: React.FC<ClientLogosSectionProps> = ({
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Marquee container - group allows pausing animation on hover */}
|
||||
<div className="relative w-full overflow-hidden group">
|
||||
{/* Inner container that will animate */}
|
||||
{/* Logos Container */}
|
||||
<div className="relative w-full overflow-hidden">
|
||||
<div
|
||||
className="flex flex-nowrap animate-marquee-continuous"
|
||||
style={{ animationDuration: `${speed}s` }}
|
||||
ref={scrollRef}
|
||||
className="flex flex-nowrap overflow-x-hidden scrollbar-hide"
|
||||
>
|
||||
{extendedClients.map((client, index) => (
|
||||
<div
|
||||
key={`${client.name}-${index}`}
|
||||
className="flex-shrink-0 mx-12 md:mx-16 py-4"
|
||||
>
|
||||
{/* Square Background Container */}
|
||||
<div
|
||||
title={client.name}
|
||||
className="
|
||||
@ -73,23 +114,35 @@ const ClientLogosSection: React.FC<ClientLogosSectionProps> = ({
|
||||
style={{
|
||||
width: squareDim,
|
||||
height: squareDim,
|
||||
padding: paddingDim, // Add padding inside the square
|
||||
padding: paddingDim,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={client.logoUrl}
|
||||
alt={`${client.name} Logo`}
|
||||
layout="fill" // Let image fill the relative container
|
||||
objectFit="contain" // Maintain aspect ratio within the container
|
||||
fill
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Optional: Add fade effect at the edges */}
|
||||
<div className="absolute inset-y-0 left-0 w-16 md:w-24 bg-gradient-to-r from-background to-transparent pointer-events-none z-10"></div>
|
||||
<div className="absolute inset-y-0 right-0 w-16 md:w-24 bg-gradient-to-l from-background to-transparent pointer-events-none z-10"></div>
|
||||
{/* Arrow Controls */}
|
||||
<div className="flex justify-center mt-8 space-x-6">
|
||||
<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>
|
||||
|
||||
{description && (
|
||||
|
||||
@ -15,6 +15,8 @@ import {
|
||||
FaUserCheck,
|
||||
FaProjectDiagram,
|
||||
} from "react-icons/fa";
|
||||
import { Metadata } from "next";
|
||||
|
||||
|
||||
// const leadershipTeam = [
|
||||
// {
|
||||
@ -42,6 +44,23 @@ import {
|
||||
// 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 = [
|
||||
{
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
import { COLORS } from "@/constants"; // Using COLORS constant
|
||||
import ContactForm from "@/components/ContactForm";
|
||||
|
||||
|
||||
// Define the structure for FAQ items
|
||||
interface FAQItem {
|
||||
id: number;
|
||||
|
||||
@ -29,6 +29,7 @@ export const metadata: Metadata = {
|
||||
"fintech solutions",
|
||||
"IT consulting",
|
||||
"OMS",
|
||||
"CVEvolve",
|
||||
"Owethu Managed Services",
|
||||
"Centurion",
|
||||
"Gauteng",
|
||||
|
||||
@ -14,6 +14,7 @@ import FeaturedProductSection, {
|
||||
} from "./_components/FeaturedProductSection";
|
||||
import { getHome } from "@/lib/query/home";
|
||||
|
||||
|
||||
export default async function HomePage() {
|
||||
// Explicitly type the data variable, assuming getHome returns HeroSectionType or null/undefined
|
||||
const data = await getHome();
|
||||
|
||||
@ -8,6 +8,7 @@ export const metadata: Metadata = {
|
||||
description:
|
||||
"Our recruitment portal is currently under development. Stay tuned for updates on career opportunities at Owethu Managed Services.",
|
||||
robots: "noindex, nofollow", // Prevent indexing of the coming soon page
|
||||
|
||||
};
|
||||
|
||||
export default function RecruitmentPortalPage() {
|
||||
|
||||
@ -25,6 +25,8 @@ export const metadata: Metadata = {
|
||||
"IT resource augmentation",
|
||||
"managed IT services",
|
||||
"OBSE",
|
||||
"CVEvolve",
|
||||
"bank statement extractor",
|
||||
"bank statement automation",
|
||||
"fintech solutions",
|
||||
"IT consulting",
|
||||
|
||||
@ -11,6 +11,7 @@ export const metadata: Metadata = {
|
||||
alternates: {
|
||||
canonical: "/tech-talk",
|
||||
},
|
||||
|
||||
openGraph: {
|
||||
title: "OMS TechTalk | Insights & Innovation",
|
||||
description: "Stay updated with tech insights from OMS.",
|
||||
|
||||
@ -12,7 +12,7 @@ interface ExtendedVacancy extends Vacancy {
|
||||
}
|
||||
|
||||
async function getVacancy(slug: string): Promise<ExtendedVacancy | null> {
|
||||
const res = await fetch(`http://localhost:3000/api/vacancies/${slug}`, {
|
||||
const res = await fetch(`${process.env.WEBSITE_URL}/api/vacancies/${slug}`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
||||
@ -208,18 +208,16 @@
|
||||
} /* Added longer delay */
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@keyframes marquee {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.8s ease-out forwards;
|
||||
opacity: 0; /* Start hidden */
|
||||
.animate-marquee-continuous {
|
||||
animation: marquee linear infinite;
|
||||
}
|
||||
|
||||
.paused {
|
||||
animation-play-state: paused !important;
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import "./globals.css";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import { ThemeProvider } from "@/providers/theme-provider";
|
||||
import Script from "next/script";
|
||||
|
||||
const poppins = Poppins({
|
||||
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
|
||||
keywords: [
|
||||
"Owethu Managed Services",
|
||||
"OMS",
|
||||
"OBSE",
|
||||
"IT solutions South Africa",
|
||||
"resource augmentation",
|
||||
@ -27,6 +29,7 @@ export const metadata: Metadata = {
|
||||
"OBSE",
|
||||
"financial data analysis",
|
||||
"IT services Centurion",
|
||||
"digital transformation",
|
||||
], // Add relevant keywords
|
||||
alternates: {
|
||||
canonical: "/", // Assuming this is the root URL
|
||||
@ -95,6 +98,14 @@ export default function RootLayout({
|
||||
<main>{children}</main>
|
||||
<Footer />
|
||||
</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>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
// components/Footer.tsx
|
||||
import React from "react";
|
||||
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";
|
||||
|
||||
|
||||
const omsLogoUrl = "/oms-logo.svg"; // Ensure this exists in /public
|
||||
// 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 */}
|
||||
<div className="flex space-x-4 mt-4">
|
||||
<a
|
||||
href="https://linkedin.com"
|
||||
href="https://www.linkedin.com/company/owethu-managed-services-oms"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--oms-white)]/70 hover:text-primary transition-colors"
|
||||
@ -135,8 +136,9 @@ const Footer = () => {
|
||||
>
|
||||
<FaLinkedin size={24} />
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://instagram.com"
|
||||
href="https://www.instagram.com/owethumanagedservices_oms"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--oms-white)]/70 hover:text-primary transition-colors"
|
||||
@ -144,6 +146,17 @@ const Footer = () => {
|
||||
>
|
||||
<FaInstagram size={24} />
|
||||
</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>
|
||||
|
||||
@ -223,6 +236,8 @@ const Footer = () => {
|
||||
>
|
||||
Privacy Policy
|
||||
</Link> */}
|
||||
|
||||
|
||||
<a
|
||||
href="/Privacy-Policy/Recruitment-Privacy-Policy.pdf"
|
||||
className="hover:text-primary transition-colors"
|
||||
@ -236,6 +251,7 @@ const Footer = () => {
|
||||
PAIA & POPIA
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
FiUsers,
|
||||
FiCpu,
|
||||
FiBox,
|
||||
FiLayers
|
||||
} from "react-icons/fi";
|
||||
import ThemeToggle from "./ThemeToggle";
|
||||
|
||||
@ -265,15 +266,16 @@ const HeaderClient = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Products */}
|
||||
<div
|
||||
className={`group ${isActive("/obse") ? "active" : ""}`} // Add active class to group
|
||||
className={`group ${isActive("/products") ? "active" : ""}`} // Add active class to group
|
||||
onMouseEnter={handleProductsMouseEnter}
|
||||
onMouseLeave={handleProductsMouseLeave}
|
||||
>
|
||||
<button
|
||||
className={`${megaMenuTriggerClasses} ${
|
||||
isActive("/obse") ? "after:w-full" : ""
|
||||
isActive("/products") ? "after:w-full" : ""
|
||||
}`}
|
||||
>
|
||||
{" "}
|
||||
@ -283,7 +285,7 @@ const HeaderClient = () => {
|
||||
</button>
|
||||
<div
|
||||
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
|
||||
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="max-w-md">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 max-w-xl">
|
||||
<Link
|
||||
href="/obse"
|
||||
className={`${megaMenuItemClasses} ${
|
||||
@ -307,7 +309,44 @@ const HeaderClient = () => {
|
||||
<FiBox className={megaMenuIconClasses} />
|
||||
<div className={megaMenuTextWrapperClasses}>
|
||||
<p className={megaMenuTitleClasses}>
|
||||
|
||||
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>
|
||||
</div>
|
||||
</Link>
|
||||
@ -315,6 +354,8 @@ const HeaderClient = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Join Our Team */}
|
||||
|
||||
|
||||
@ -425,6 +466,11 @@ const HeaderClient = () => {
|
||||
OBSE
|
||||
</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">
|
||||
Join Us
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user