mirror of
https://github.com/OwethuManagedServices/oms-website-nextjs.git
synced 2025-12-17 19:08:09 +00:00
feature: contact us fixed
This commit is contained in:
@ -1,7 +1,17 @@
|
|||||||
import React from "react";
|
"use client"; // Needed for FAQ state
|
||||||
import type { Metadata } from "next";
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
FaMapMarkerAlt,
|
||||||
|
FaPhone,
|
||||||
|
FaEnvelope,
|
||||||
|
FaClock,
|
||||||
|
FaChevronDown,
|
||||||
|
FaChevronUp,
|
||||||
|
} from "react-icons/fa"; // Using Fa icons for consistency
|
||||||
|
import { COLORS } from "@/constants"; // Using COLORS constant
|
||||||
import ContactForm from "@/components/ContactForm";
|
import ContactForm from "@/components/ContactForm";
|
||||||
import { FiMapPin, FiPhone, FiMail, FiClock } from "react-icons/fi"; // Import icons
|
import { Metadata } from "next";
|
||||||
|
|
||||||
// SEO Metadata for the Contact page
|
// SEO Metadata for the Contact page
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@ -32,17 +42,62 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Define the structure for FAQ items
|
||||||
|
interface FAQItem {
|
||||||
|
id: number;
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const faqData: FAQItem[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
question: "What services does Owethu Managed Services offer?",
|
||||||
|
answer:
|
||||||
|
"We offer a comprehensive range of IT services including custom software development, resource augmentation (IT staffing), project management, cloud solutions, system integration, and IT consulting tailored to various industries.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
question: "Which industries do you specialize in?",
|
||||||
|
answer:
|
||||||
|
"We have deep expertise in Financial Services & Fintech, Automotive, Technology, and Retail/E-commerce sectors, understanding the unique challenges and opportunities within each.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
question: "How can I request a quote for a project?",
|
||||||
|
answer:
|
||||||
|
"You can request a quote by filling out the contact form on this page with details about your project requirements, calling us directly, or sending an email to hello@oms.africa. We'll get back to you promptly to discuss your needs.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
question: "What are your business hours?",
|
||||||
|
answer:
|
||||||
|
"Our standard business hours are Monday to Friday, 8:00 AM to 5:00 PM South African Standard Time (SAST). We are closed on weekends and public holidays.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Contact Page Component
|
// Contact Page Component
|
||||||
export default function ContactPage() {
|
export default function ContactPage() {
|
||||||
|
const [openFaqId, setOpenFaqId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const toggleFaq = (id: number) => {
|
||||||
|
setOpenFaqId(openFaqId === id ? null : id);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-gradient-to-b from-background to-secondary/50 text-foreground">
|
// Added dark mode base styles
|
||||||
{/* Hero Section */}
|
<div className="bg-gray-50 text-gray-800 dark:bg-gray-900 dark:text-gray-200 overflow-x-hidden font-poppins">
|
||||||
<section className="py-20 md:py-28 text-center bg-primary/10 dark:bg-primary/5 border-b border-border">
|
{/* Hero Section - Adjusted padding for mobile */}
|
||||||
<div className="container mx-auto px-4">
|
<section className="relative bg-gradient-to-r from-gray-800 via-gray-700 to-gray-800 text-white py-16 md:py-28">
|
||||||
<h1 className="text-4xl md:text-5xl font-bold mb-4 text-primary">
|
<div className="absolute inset-0 bg-black opacity-40 dark:opacity-50"></div>
|
||||||
|
<div className="container mx-auto px-4 md:px-6 text-center relative z-10">
|
||||||
|
<h1
|
||||||
|
className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold mb-4 font-poppins drop-shadow-md dark:text-gold-400" // Use primary color directly or a dark-mode friendly version
|
||||||
|
style={{ color: COLORS.primary }} // Keep gold color for heading, ensure contrast in dark mode
|
||||||
|
>
|
||||||
Get In Touch
|
Get In Touch
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto">
|
<p className="text-base sm:text-lg md:text-xl max-w-3xl mx-auto leading-relaxed text-gray-200 dark:text-gray-300 font-poppins">
|
||||||
We're here to help! Whether you have a question about our
|
We're here to help! Whether you have a question about our
|
||||||
services, need assistance, or want to discuss a project, reach out
|
services, need assistance, or want to discuss a project, reach out
|
||||||
and let us know.
|
and let us know.
|
||||||
@ -50,23 +105,31 @@ export default function ContactPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Main Content Area (Form + Info) */}
|
{/* Main Content Area (Form + Info) - Adjusted padding, added dark mode */}
|
||||||
<section className="py-16 md:py-24">
|
<section className="py-12 md:py-24 bg-white dark:bg-gray-800">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4 md:px-6">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 md:gap-16 lg:gap-20 items-start">
|
{/* Grid stacks vertically by default, becomes 2 columns on large screens */}
|
||||||
{/* Contact Information Section */}
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 md:gap-16 items-start">
|
||||||
<div className="space-y-8 bg-card p-8 rounded-lg border border-border shadow-sm">
|
{/* Contact Information Section - Added dark mode styles */}
|
||||||
<h2 className="text-3xl font-semibold mb-6 text-foreground">
|
<div
|
||||||
|
className="bg-gray-50 dark:bg-gray-700 p-6 md:p-8 rounded-xl shadow-md border-t-4 border-gold-500 dark:border-gold-400 space-y-6 md:space-y-8"
|
||||||
|
style={{ borderColor: COLORS.primary }} // Keep primary border color
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold font-poppins text-gray-900 dark:text-white mb-6">
|
||||||
Contact Information
|
Contact Information
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="flex items-start space-x-4">
|
<div className="flex items-start space-x-4">
|
||||||
<FiMapPin className="w-6 h-6 text-primary mt-1 flex-shrink-0" />
|
<FaMapMarkerAlt
|
||||||
|
className="w-6 h-6 text-gold-500 mt-1 flex-shrink-0"
|
||||||
|
style={{ color: COLORS.primary }} // Keep primary icon color
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium text-foreground">
|
<h3 className="text-lg font-semibold font-poppins text-gray-800 dark:text-gray-100">
|
||||||
Our Office
|
Our Office
|
||||||
</h3>
|
</h3>
|
||||||
<address className="text-muted-foreground not-italic text-sm leading-relaxed">
|
<address className="text-gray-600 dark:text-gray-300 font-poppins not-italic text-sm leading-relaxed">
|
||||||
|
{/* ... existing address lines ... */}
|
||||||
Unit 10 B Centuria Park
|
Unit 10 B Centuria Park
|
||||||
<br />
|
<br />
|
||||||
265 Von Willich Avenue
|
265 Von Willich Avenue
|
||||||
@ -75,12 +138,12 @@ export default function ContactPage() {
|
|||||||
<br />
|
<br />
|
||||||
South Africa
|
South Africa
|
||||||
</address>
|
</address>
|
||||||
{/* Optional: Link to Google Maps */}
|
|
||||||
<a
|
<a
|
||||||
href="https://www.google.com/maps/place/Owethu+Managed+Services/@-25.863168,28.186075,17z/data=!3m1!4b1!4m6!3m5!1s0x1e9565b7018f5f9b:0x4d8a4ae3a2c0d9a1!8m2!3d-25.8631728!4d28.1886499!16s%2Fg%2F11h1_q_1_f?entry=ttu" // Replace with your actual Google Maps link
|
href="https://www.google.com/maps/place/Owethu+Managed+Services/@-25.863168,28.186075,17z/data=!3m1!4b1!4m6!3m5!1s0x1e9565b7018f5f9b:0x4d8a4ae3a2c0d9a1!8m2!3d-25.8631728!4d28.1886499!16s%2Fg%2F11h1_q_1_f?entry=ttu"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-sm text-primary hover:underline mt-2 inline-block"
|
className="text-sm font-medium font-poppins hover:underline mt-2 inline-block dark:text-gold-400 dark:hover:text-gold-300"
|
||||||
|
style={{ color: COLORS.primary }} // Keep primary link color
|
||||||
>
|
>
|
||||||
View on Google Maps
|
View on Google Maps
|
||||||
</a>
|
</a>
|
||||||
@ -88,12 +151,17 @@ export default function ContactPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start space-x-4">
|
<div className="flex items-start space-x-4">
|
||||||
<FiPhone className="w-6 h-6 text-primary mt-1 flex-shrink-0" />
|
<FaPhone
|
||||||
|
className="w-5 h-5 text-gold-500 mt-1 flex-shrink-0"
|
||||||
|
style={{ color: COLORS.primary }} // Keep primary icon color
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium text-foreground">Phone</h3>
|
<h3 className="text-lg font-semibold font-poppins text-gray-800 dark:text-gray-100">
|
||||||
|
Phone
|
||||||
|
</h3>
|
||||||
<a
|
<a
|
||||||
href="tel:+27120513282"
|
href="tel:+27120513282"
|
||||||
className="text-muted-foreground hover:text-primary transition text-sm"
|
className="text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100 transition text-sm font-poppins"
|
||||||
>
|
>
|
||||||
(012) 051 3282
|
(012) 051 3282
|
||||||
</a>
|
</a>
|
||||||
@ -101,12 +169,17 @@ export default function ContactPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start space-x-4">
|
<div className="flex items-start space-x-4">
|
||||||
<FiMail className="w-6 h-6 text-primary mt-1 flex-shrink-0" />
|
<FaEnvelope
|
||||||
|
className="w-5 h-5 text-gold-500 mt-1 flex-shrink-0"
|
||||||
|
style={{ color: COLORS.primary }} // Keep primary icon color
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium text-foreground">Email</h3>
|
<h3 className="text-lg font-semibold font-poppins text-gray-800 dark:text-gray-100">
|
||||||
|
Email
|
||||||
|
</h3>
|
||||||
<a
|
<a
|
||||||
href="mailto:hello@oms.africa"
|
href="mailto:hello@oms.africa"
|
||||||
className="text-muted-foreground hover:text-primary transition text-sm"
|
className="text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100 transition text-sm font-poppins"
|
||||||
>
|
>
|
||||||
hello@oms.africa
|
hello@oms.africa
|
||||||
</a>
|
</a>
|
||||||
@ -114,38 +187,109 @@ export default function ContactPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start space-x-4">
|
<div className="flex items-start space-x-4">
|
||||||
<FiClock className="w-6 h-6 text-primary mt-1 flex-shrink-0" />
|
<FaClock
|
||||||
|
className="w-5 h-5 text-gold-500 mt-1 flex-shrink-0"
|
||||||
|
style={{ color: COLORS.primary }} // Keep primary icon color
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium text-foreground">
|
<h3 className="text-lg font-semibold font-poppins text-gray-800 dark:text-gray-100">
|
||||||
Business Hours
|
Business Hours
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-gray-600 dark:text-gray-300 text-sm font-poppins">
|
||||||
Monday - Friday: 8:00 AM - 5:00 PM (SAST)
|
Monday - Friday: 8:00 AM - 5:00 PM (SAST)
|
||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-gray-600 dark:text-gray-300 text-sm font-poppins">
|
||||||
Weekends & Public Holidays: Closed
|
Weekends & Public Holidays: Closed
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contact Form Section */}
|
{/* Contact Form Section - Added dark mode styles */}
|
||||||
<div className="bg-card p-8 rounded-lg border border-border shadow-sm">
|
<div
|
||||||
<h2 className="text-3xl font-semibold mb-6 text-foreground">
|
className="bg-gray-50 dark:bg-gray-700 p-6 md:p-8 rounded-xl shadow-md border-t-4 border-gold-500 dark:border-gold-400"
|
||||||
|
style={{ borderColor: COLORS.primary }} // Keep primary border color
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold font-poppins text-gray-900 dark:text-white mb-6">
|
||||||
Send Us a Message
|
Send Us a Message
|
||||||
</h2>
|
</h2>
|
||||||
|
{/* Assuming ContactForm handles its own dark mode or inherits text colors */}
|
||||||
<ContactForm />
|
<ContactForm />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Optional: Map Section Placeholder */}
|
{/* Map Section - Adjusted padding, added dark mode, responsive height */}
|
||||||
{/* <section className="h-96 bg-muted border-t border-border">
|
<section className="py-12 md:py-24 bg-gray-100 dark:bg-gray-900">
|
||||||
<div className="container mx-auto h-full flex items-center justify-center">
|
<div className="container mx-auto px-4 md:px-6">
|
||||||
<p className="text-muted-foreground">[Embedded Map Placeholder - e.g., Google Maps iframe]</p>
|
<h2 className="text-2xl md:text-3xl lg:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-8 text-center">
|
||||||
|
Find Us Here
|
||||||
|
</h2>
|
||||||
|
{/* Adjusted height for different screen sizes */}
|
||||||
|
<div className="aspect-w-16 aspect-h-9 h-64 sm:h-80 md:h-96 mx-auto rounded-lg overflow-hidden shadow-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
<iframe
|
||||||
|
// ... existing iframe attributes ...
|
||||||
|
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d1313.49241475678!2d28.192648350415848!3d-25.85177958049519!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x1e95643aa4e82f9f%3A0xe4052722532cd30f!2sCenturia%20Park!5e1!3m2!1sen!2sza!4v1745745054858!5m2!1sen!2sza"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
style={{ border: 0 }}
|
||||||
|
allowFullScreen={true}
|
||||||
|
loading="lazy"
|
||||||
|
referrerPolicy="no-referrer-when-downgrade"
|
||||||
|
title="Owethu Managed Services Location"
|
||||||
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
</section> */}
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* FAQ Section - Adjusted padding, added dark mode */}
|
||||||
|
<section className="py-12 md:py-24 bg-white dark:bg-gray-800">
|
||||||
|
<div className="container mx-auto px-4 md:px-6">
|
||||||
|
<h2 className="text-2xl md:text-3xl lg:text-4xl font-bold font-poppins text-gray-900 dark:text-white mb-10 md:mb-12 text-center">
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
|
<div className="max-w-5xl mx-auto space-y-4">
|
||||||
|
{faqData.map((faq) => (
|
||||||
|
<div
|
||||||
|
key={faq.id}
|
||||||
|
className="border border-gray-200 dark:border-gray-600 rounded-lg overflow-hidden shadow-sm bg-white dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleFaq(faq.id)}
|
||||||
|
className="flex justify-between items-center w-full p-4 text-left font-semibold font-poppins text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gold-500 dark:focus:ring-gold-400 focus:ring-opacity-50"
|
||||||
|
aria-expanded={openFaqId === faq.id}
|
||||||
|
aria-controls={`faq-answer-${faq.id}`}
|
||||||
|
>
|
||||||
|
<span className="text-sm md:text-base">{faq.question}</span>
|
||||||
|
{openFaqId === faq.id ? (
|
||||||
|
<FaChevronUp
|
||||||
|
className="w-5 h-5 text-gold-500 flex-shrink-0 ml-2" // Added flex-shrink-0 and margin
|
||||||
|
style={{ color: COLORS.primary }} // Keep primary icon color
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FaChevronDown
|
||||||
|
className="w-5 h-5 text-gold-500 flex-shrink-0 ml-2" // Added flex-shrink-0 and margin
|
||||||
|
style={{ color: COLORS.primary }} // Keep primary icon color
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
id={`faq-answer-${faq.id}`}
|
||||||
|
// Adjusted padding and text size
|
||||||
|
className={`px-4 pb-4 pt-2 text-gray-600 dark:text-gray-300 font-poppins text-xs sm:text-sm leading-relaxed transition-all duration-300 ease-in-out ${
|
||||||
|
openFaqId === faq.id ? "block" : "hidden"
|
||||||
|
}`}
|
||||||
|
role="region"
|
||||||
|
aria-labelledby={`faq-question-${faq.id}`}
|
||||||
|
>
|
||||||
|
{faq.answer}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
239
app/(website)/vacancies/page.tsx
Normal file
239
app/(website)/vacancies/page.tsx
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
"use client"; // <-- Add this line to use state
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState } from "react"; // <-- Import useState
|
||||||
|
import {
|
||||||
|
FaMapMarkerAlt,
|
||||||
|
FaBriefcase,
|
||||||
|
FaPaperPlane,
|
||||||
|
FaArrowRight,
|
||||||
|
FaRegClock,
|
||||||
|
FaSearch,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
import { demoVacancies } from "@/lib/demo-data/vacancies";
|
||||||
|
import { Vacancy } from "@/types";
|
||||||
|
import Button from "@/components/ui/Button";
|
||||||
|
import Modal from "@/components/ui/Modal"; // <-- Import your Modal component
|
||||||
|
import { COLORS } from "@/constants";
|
||||||
|
import VacancyApplicationForm from "@/components/VacancyApplicationForm";
|
||||||
|
|
||||||
|
// Metadata object might need adjustment depending on your setup with client components
|
||||||
|
// If using App Router, keep it, Next.js handles it.
|
||||||
|
/*
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Current Vacancies | OMS",
|
||||||
|
description:
|
||||||
|
"Explore exciting career opportunities at OMS. Find your perfect role or submit your CV for future consideration.",
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Define gold color for consistency (if COLORS.primary is not '#e1c44a', adjust accordingly)
|
||||||
|
const goldColor = COLORS.primary || "#e1c44a"; // Use COLORS.primary or fallback
|
||||||
|
|
||||||
|
// --- VacancyCard Component (no changes needed here) ---
|
||||||
|
interface VacancyCardProps {
|
||||||
|
vacancy: Vacancy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VacancyCard({ vacancy }: VacancyCardProps) {
|
||||||
|
const formatDate = (dateString: string | undefined) => {
|
||||||
|
if (!dateString) return "Date N/A";
|
||||||
|
try {
|
||||||
|
return new Date(dateString).toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return "Invalid Date";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const postedDate = formatDate(vacancy.postedDate);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/vacancies/${vacancy.slug}`}
|
||||||
|
className="group block w-full transform transition duration-300 ease-in-out hover:-translate-y-1" // Add subtle lift on hover
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="relative flex flex-col h-full overflow-hidden rounded-lg bg-white dark:bg-gray-800 p-6 shadow-md transition-shadow duration-300 hover:shadow-xl border-l-4 dark:border-l-yellow-500" // Card base style + left border + dark mode
|
||||||
|
style={{ borderColor: goldColor }} // Apply gold border color (consider dark mode alternative if needed)
|
||||||
|
>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<h3 className="mb-2 text-xl font-bold font-poppins text-gray-900 dark:text-gray-100 transition-colors group-hover:text-gray-700 dark:group-hover:text-gray-300">
|
||||||
|
{vacancy.title}
|
||||||
|
</h3>
|
||||||
|
<div className="mb-4 flex flex-col space-y-2 text-sm text-gray-600 dark:text-gray-400 font-poppins">
|
||||||
|
<span className="inline-flex items-center gap-2">
|
||||||
|
<FaMapMarkerAlt
|
||||||
|
className="h-4 w-4 flex-shrink-0"
|
||||||
|
style={{ color: goldColor }} // Keep gold or use dark:text-yellow-400
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{vacancy.location.city}{" "}
|
||||||
|
{vacancy.location.remote && (
|
||||||
|
<span className="ml-1 rounded bg-gray-200 dark:bg-gray-700 px-1.5 py-0.5 text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Remote
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center gap-2">
|
||||||
|
<FaBriefcase
|
||||||
|
className="h-4 w-4 flex-shrink-0"
|
||||||
|
style={{ color: goldColor }} // Keep gold or use dark:text-yellow-400
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{vacancy.employmentType}
|
||||||
|
</span>
|
||||||
|
{vacancy.postedDate && (
|
||||||
|
<span className="inline-flex items-center gap-2">
|
||||||
|
<FaRegClock
|
||||||
|
className="h-4 w-4 flex-shrink-0"
|
||||||
|
style={{ color: goldColor }} // Keep gold or use dark:text-yellow-400
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
Posted: {postedDate}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-auto pt-4 border-t border-gray-100 dark:border-gray-700">
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center text-sm font-medium font-poppins"
|
||||||
|
style={{ color: goldColor }} // Keep gold or use dark:text-yellow-400
|
||||||
|
>
|
||||||
|
View Details
|
||||||
|
<FaArrowRight
|
||||||
|
className="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// --- End Vacancy Card Component ---
|
||||||
|
|
||||||
|
// --- Vacancies Page ---
|
||||||
|
export default function VacanciesPage() {
|
||||||
|
// TODO: Replace demoVacancies with actual API call if needed client-side,
|
||||||
|
// or fetch server-side and pass as props if using Pages Router.
|
||||||
|
// For App Router, `async function` fetches server-side by default.
|
||||||
|
const vacancies = demoVacancies;
|
||||||
|
|
||||||
|
// --- State for the "Future Positions" Modal ---
|
||||||
|
const [isFuturePositionModalOpen, setIsFuturePositionModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const handleOpenFuturePositionModal = () =>
|
||||||
|
setIsFuturePositionModalOpen(true);
|
||||||
|
const handleCloseFuturePositionModal = () =>
|
||||||
|
setIsFuturePositionModalOpen(false);
|
||||||
|
// --- End State ---
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-200 overflow-x-hidden font-poppins">
|
||||||
|
{/* Section 1: Hero / Page Header */}
|
||||||
|
<section className="relative bg-gradient-to-r from-gray-800 via-gray-700 to-gray-800 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 text-white py-20 md:py-28">
|
||||||
|
<div className="absolute inset-0 bg-black opacity-30 dark:opacity-50"></div>
|
||||||
|
<div className="container mx-auto px-6 text-center relative z-10">
|
||||||
|
<h1
|
||||||
|
className="text-4xl md:text-5xl lg:text-6xl font-bold mb-4 font-poppins drop-shadow-md"
|
||||||
|
style={{ color: goldColor }} // Keep gold or use dark:text-yellow-400
|
||||||
|
>
|
||||||
|
Career Opportunities
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed text-gray-200 dark:text-gray-300 font-poppins">
|
||||||
|
Join our team of innovators and experts. Explore current openings at
|
||||||
|
OMS or submit your CV for future consideration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Section 2: Vacancy List */}
|
||||||
|
<section className="py-16 md:py-24">
|
||||||
|
<div className="container mx-auto px-6">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold font-poppins text-gray-900 dark:text-gray-100 mb-12 text-center">
|
||||||
|
Current Openings
|
||||||
|
</h2>
|
||||||
|
{vacancies.length > 0 ? (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{vacancies.map((vacancy) => (
|
||||||
|
<VacancyCard key={vacancy.id} vacancy={vacancy} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-10 max-w-2xl mx-auto rounded-lg border border-dashed border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 p-10 text-center shadow-sm">
|
||||||
|
<FaSearch
|
||||||
|
className="mx-auto mb-5 h-12 w-12"
|
||||||
|
style={{ color: goldColor }} // Keep gold or use dark:text-yellow-400
|
||||||
|
/>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-800 dark:text-gray-100 font-poppins mb-2">
|
||||||
|
No Open Vacancies Right Now
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 font-poppins">
|
||||||
|
We're not actively hiring for specific roles at the moment,
|
||||||
|
but we're always looking for passionate and talented
|
||||||
|
individuals. Check back soon or submit your CV below!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Section 3: Future Positions / CV Submission */}
|
||||||
|
<section
|
||||||
|
className="py-16 md:py-24 text-gray-900 dark:text-gray-800" // Adjust text color for dark mode contrast on gold bg
|
||||||
|
style={{ backgroundColor: goldColor }} // Keep gold background
|
||||||
|
>
|
||||||
|
<div className="container mx-auto px-6 text-center">
|
||||||
|
<FaPaperPlane className="text-5xl mx-auto mb-5 text-gray-800 dark:text-gray-900" />{" "}
|
||||||
|
{/* Ensure icon contrast */}
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-4 font-poppins text-gray-900 dark:text-gray-900">
|
||||||
|
{" "}
|
||||||
|
{/* Ensure heading contrast */}
|
||||||
|
Don't See the Right Fit?
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg md:text-xl max-w-3xl mx-auto leading-relaxed font-poppins text-gray-800 dark:text-gray-800">
|
||||||
|
{" "}
|
||||||
|
{/* Ensure text contrast */}
|
||||||
|
We're always looking for talented individuals to join our
|
||||||
|
journey. Submit your CV, and we'll keep you in mind for future
|
||||||
|
openings that match your profile.
|
||||||
|
</p>
|
||||||
|
<div className="mt-10">
|
||||||
|
{/* --- Updated Button --- */}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleOpenFuturePositionModal}
|
||||||
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
|
className="inline-flex items-center gap-2 group font-poppins bg-gray-800 text-black hover:bg-gray-900 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200" // Adjusted dark mode button colors
|
||||||
|
>
|
||||||
|
Submit Your CV
|
||||||
|
<FaArrowRight className="h-4 w-4 transition-transform duration-200 group-hover:translate-x-1" />
|
||||||
|
</Button>
|
||||||
|
{/* --- End Updated Button --- */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* --- Modal for Future Position Application --- */}
|
||||||
|
<Modal
|
||||||
|
isOpen={isFuturePositionModalOpen}
|
||||||
|
onClose={handleCloseFuturePositionModal}
|
||||||
|
title="Apply for Future Positions"
|
||||||
|
size="4xl"
|
||||||
|
>
|
||||||
|
<VacancyApplicationForm
|
||||||
|
vacancyId="future-position"
|
||||||
|
vacancyTitle="General Application / Future Position"
|
||||||
|
onClose={handleCloseFuturePositionModal}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,40 +1,54 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useActionState, useEffect, useRef } from "react";
|
import React, { useEffect, useActionState } from "react"; // Removed useRef
|
||||||
import { useFormStatus } from "react-dom";
|
import { useForm } from "react-hook-form"; // Removed SubmitHandler
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
import { submitContactForm, ContactFormState } from "@/actions/contact";
|
import { submitContactForm, ContactFormState } from "@/actions/contact";
|
||||||
import Button from "@/components/ui/Button"; // Use your existing Button component
|
import Button from "@/components/ui/Button";
|
||||||
|
|
||||||
// Submit button component with pending state
|
// Zod schema for contact form validation
|
||||||
function SubmitButton() {
|
const contactSchema = z.object({
|
||||||
const { pending } = useFormStatus();
|
name: z.string().min(2, "Full name must be at least 2 characters"),
|
||||||
return (
|
email: z.string().email("Invalid email address"),
|
||||||
<Button type="submit" variant="primary" size="lg" disabled={pending}>
|
subject: z.string().min(3, "Subject must be at least 3 characters"),
|
||||||
{pending ? "Sending..." : "Send Message"}
|
message: z.string().min(10, "Message must be at least 10 characters"),
|
||||||
</Button>
|
});
|
||||||
);
|
|
||||||
}
|
type ContactFormData = z.infer<typeof contactSchema>;
|
||||||
|
|
||||||
// The main contact form component
|
|
||||||
export default function ContactForm() {
|
export default function ContactForm() {
|
||||||
|
// React Hook Form setup
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors, isValid },
|
||||||
|
reset,
|
||||||
|
} = useForm<ContactFormData>({
|
||||||
|
resolver: zodResolver(contactSchema),
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
|
||||||
const initialState: ContactFormState = {
|
const initialState: ContactFormState = {
|
||||||
message: null,
|
message: null,
|
||||||
errors: {},
|
errors: {},
|
||||||
success: false,
|
success: false,
|
||||||
};
|
};
|
||||||
const [state, dispatch] = useActionState(submitContactForm, initialState);
|
const [state, formAction] = useActionState(submitContactForm, initialState); // Renamed dispatch to formAction for clarity
|
||||||
const formRef = useRef<HTMLFormElement>(null); // Ref to reset the form
|
|
||||||
|
|
||||||
// Reset form on successful submission
|
// Reset form when server action reports success
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.success) {
|
if (state.success) {
|
||||||
formRef.current?.reset();
|
reset();
|
||||||
}
|
}
|
||||||
}, [state.success]);
|
}, [state.success, reset]);
|
||||||
|
|
||||||
|
// Removed onSubmit handler
|
||||||
|
|
||||||
|
// Pass formAction directly to the form's action prop
|
||||||
|
// Remove onSubmit={handleSubmit(onSubmit)}
|
||||||
return (
|
return (
|
||||||
<form ref={formRef} action={dispatch} className="space-y-6">
|
<form action={formAction} className="space-y-6">
|
||||||
{/* Name Input */}
|
{/* Name Field */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="name"
|
htmlFor="name"
|
||||||
@ -43,24 +57,40 @@ export default function ContactForm() {
|
|||||||
Full Name
|
Full Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
type="text"
|
||||||
required
|
{...register("name")}
|
||||||
className="block w-full px-4 py-2 border border-border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
|
aria-invalid={errors.name ? "true" : "false"}
|
||||||
aria-describedby="name-error"
|
aria-describedby="name-error"
|
||||||
|
className={`block w-full px-4 py-2 border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:placeholder-gray-400 ${
|
||||||
|
errors.name
|
||||||
|
? "border-destructive"
|
||||||
|
: "border-border dark:border-gray-600"
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
<div id="name-error" aria-live="polite" aria-atomic="true">
|
{/* Client-side error */}
|
||||||
|
{errors.name && (
|
||||||
|
<p
|
||||||
|
id="name-error"
|
||||||
|
role="alert"
|
||||||
|
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||||
|
>
|
||||||
|
{errors.name.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{/* Server-side error */}
|
||||||
{state.errors?.name &&
|
{state.errors?.name &&
|
||||||
state.errors.name.map((error: string) => (
|
state.errors.name.map((err: string) => (
|
||||||
<p className="mt-1 text-sm text-destructive" key={error}>
|
<p
|
||||||
{error}
|
key={err}
|
||||||
|
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||||
|
>
|
||||||
|
{err}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Email Input */}
|
{/* Email Field */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="email"
|
htmlFor="email"
|
||||||
@ -69,24 +99,38 @@ export default function ContactForm() {
|
|||||||
Email Address
|
Email Address
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
type="email"
|
||||||
required
|
{...register("email")}
|
||||||
className="block w-full px-4 py-2 border border-border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
|
aria-invalid={errors.email ? "true" : "false"}
|
||||||
aria-describedby="email-error"
|
aria-describedby="email-error"
|
||||||
|
className={`block w-full px-4 py-2 border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:placeholder-gray-400 ${
|
||||||
|
errors.email
|
||||||
|
? "border-destructive"
|
||||||
|
: "border-border dark:border-gray-600"
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
<div id="email-error" aria-live="polite" aria-atomic="true">
|
{errors.email && (
|
||||||
|
<p
|
||||||
|
id="email-error"
|
||||||
|
role="alert"
|
||||||
|
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||||
|
>
|
||||||
|
{errors.email.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{state.errors?.email &&
|
{state.errors?.email &&
|
||||||
state.errors.email.map((error: string) => (
|
state.errors.email.map((err: string) => (
|
||||||
<p className="mt-1 text-sm text-destructive" key={error}>
|
<p
|
||||||
{error}
|
key={err}
|
||||||
|
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||||
|
>
|
||||||
|
{err}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Subject Input */}
|
{/* Subject Field */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="subject"
|
htmlFor="subject"
|
||||||
@ -95,24 +139,38 @@ export default function ContactForm() {
|
|||||||
Subject
|
Subject
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
|
||||||
id="subject"
|
id="subject"
|
||||||
name="subject"
|
type="text"
|
||||||
required
|
{...register("subject")}
|
||||||
className="block w-full px-4 py-2 border border-border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
|
aria-invalid={errors.subject ? "true" : "false"}
|
||||||
aria-describedby="subject-error"
|
aria-describedby="subject-error"
|
||||||
|
className={`block w-full px-4 py-2 border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:placeholder-gray-400 ${
|
||||||
|
errors.subject
|
||||||
|
? "border-destructive"
|
||||||
|
: "border-border dark:border-gray-600"
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
<div id="subject-error" aria-live="polite" aria-atomic="true">
|
{errors.subject && (
|
||||||
|
<p
|
||||||
|
id="subject-error"
|
||||||
|
role="alert"
|
||||||
|
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||||
|
>
|
||||||
|
{errors.subject.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{state.errors?.subject &&
|
{state.errors?.subject &&
|
||||||
state.errors.subject.map((error: string) => (
|
state.errors.subject.map((err: string) => (
|
||||||
<p className="mt-1 text-sm text-destructive" key={error}>
|
<p
|
||||||
{error}
|
key={err}
|
||||||
|
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||||
|
>
|
||||||
|
{err}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Message Textarea */}
|
{/* Message Field */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="message"
|
htmlFor="message"
|
||||||
@ -122,38 +180,65 @@ export default function ContactForm() {
|
|||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="message"
|
id="message"
|
||||||
name="message"
|
|
||||||
rows={5}
|
rows={5}
|
||||||
required
|
{...register("message")}
|
||||||
className="block w-full px-4 py-2 border border-border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
|
aria-invalid={errors.message ? "true" : "false"}
|
||||||
aria-describedby="message-error"
|
aria-describedby="message-error"
|
||||||
></textarea>
|
className={`block w-full px-4 py-2 border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground resize-vertical dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:placeholder-gray-400 ${
|
||||||
<div id="message-error" aria-live="polite" aria-atomic="true">
|
errors.message
|
||||||
|
? "border-destructive"
|
||||||
|
: "border-border dark:border-gray-600"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{errors.message && (
|
||||||
|
<p
|
||||||
|
id="message-error"
|
||||||
|
role="alert"
|
||||||
|
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||||
|
>
|
||||||
|
{errors.message.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{state.errors?.message &&
|
{state.errors?.message &&
|
||||||
state.errors.message.map((error: string) => (
|
state.errors.message.map((err: string) => (
|
||||||
<p className="mt-1 text-sm text-destructive" key={error}>
|
<p
|
||||||
{error}
|
key={err}
|
||||||
|
className="mt-1 text-sm text-destructive dark:text-red-400"
|
||||||
|
>
|
||||||
|
{err}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* General Form Message (Success or Error) */}
|
{/* General Form Response */}
|
||||||
<div id="form-response" aria-live="polite" aria-atomic="true">
|
|
||||||
{state.message && (
|
{state.message && (
|
||||||
<p
|
<p
|
||||||
className={`text-sm ${
|
className={`text-sm ${
|
||||||
state.success ? "text-green-600" : "text-destructive"
|
state.success
|
||||||
|
? "text-green-600 dark:text-green-400"
|
||||||
|
: "text-destructive dark:text-red-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{state.message}
|
{state.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
|
{/* isSubmitting from useFormState is now implicitly handled by the form action */}
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<SubmitButton />
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
// Disable based on client-side validation only before submission starts
|
||||||
|
// The form handles disabling during submission automatically
|
||||||
|
disabled={!isValid}
|
||||||
|
// aria-disabled={pending} // You might need to import useFormStatus for pending state
|
||||||
|
>
|
||||||
|
{/* {pending ? "Sending..." : "Send Message"} // Use useFormStatus for pending state */}
|
||||||
|
Send Message{" "}
|
||||||
|
{/* Simplified button text, pending state handled by browser/React */}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user