diff --git a/app/(website)/contact/page.tsx b/app/(website)/contact/page.tsx index 3469adf..cfba5a7 100644 --- a/app/(website)/contact/page.tsx +++ b/app/(website)/contact/page.tsx @@ -1,7 +1,17 @@ -import React from "react"; -import type { Metadata } from "next"; +"use client"; // Needed for FAQ state + +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 { FiMapPin, FiPhone, FiMail, FiClock } from "react-icons/fi"; // Import icons +import { Metadata } from "next"; // SEO Metadata for the Contact page 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 export default function ContactPage() { + const [openFaqId, setOpenFaqId] = useState(null); + + const toggleFaq = (id: number) => { + setOpenFaqId(openFaqId === id ? null : id); + }; + return ( -
- {/* Hero Section */} -
-
-

+ // Added dark mode base styles +
+ {/* Hero Section - Adjusted padding for mobile */} +
+
+
+

Get In Touch

-

+

We're here to help! Whether you have a question about our services, need assistance, or want to discuss a project, reach out and let us know. @@ -50,23 +105,31 @@ export default function ContactPage() {

- {/* Main Content Area (Form + Info) */} -
-
-
- {/* Contact Information Section */} -
-

+ {/* Main Content Area (Form + Info) - Adjusted padding, added dark mode */} +
+
+ {/* Grid stacks vertically by default, becomes 2 columns on large screens */} +
+ {/* Contact Information Section - Added dark mode styles */} +
+

Contact Information

- +
-

+

Our Office

-
+
+ {/* ... existing address lines ... */} Unit 10 B Centuria Park
265 Von Willich Avenue @@ -75,12 +138,12 @@ export default function ContactPage() {
South Africa
- {/* Optional: Link to Google Maps */} View on Google Maps @@ -88,12 +151,17 @@ export default function ContactPage() {
- +
-

Phone

+

+ Phone +

(012) 051 3282 @@ -101,12 +169,17 @@ export default function ContactPage() {
- +
-

Email

+

+ Email +

hello@oms.africa @@ -114,38 +187,109 @@ export default function ContactPage() {
- +
-

+

Business Hours

-

+

Monday - Friday: 8:00 AM - 5:00 PM (SAST)

-

+

Weekends & Public Holidays: Closed

- {/* Contact Form Section */} -
-

+ {/* Contact Form Section - Added dark mode styles */} +
+

Send Us a Message

+ {/* Assuming ContactForm handles its own dark mode or inherits text colors */}

- {/* Optional: Map Section Placeholder */} - {/*
-
-

[Embedded Map Placeholder - e.g., Google Maps iframe]

-
-
*/} + {/* Map Section - Adjusted padding, added dark mode, responsive height */} +
+
+

+ Find Us Here +

+ {/* Adjusted height for different screen sizes */} +
+ +
+
+
+ + {/* FAQ Section - Adjusted padding, added dark mode */} +
+
+

+ Frequently Asked Questions +

+
+ {faqData.map((faq) => ( +
+ +
+ {faq.answer} +
+
+ ))} +
+
+

); } diff --git a/app/(website)/vacancies/page.tsx b/app/(website)/vacancies/page.tsx new file mode 100644 index 0000000..cc61e4b --- /dev/null +++ b/app/(website)/vacancies/page.tsx @@ -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 ( + +
+
+

+ {vacancy.title} +

+
+ + + + + {vacancy.postedDate && ( + + + )} +
+
+
+ + View Details + +
+
+ + ); +} +// --- 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 ( +
+ {/* Section 1: Hero / Page Header */} +
+
+
+

+ Career Opportunities +

+

+ Join our team of innovators and experts. Explore current openings at + OMS or submit your CV for future consideration. +

+
+
+ + {/* Section 2: Vacancy List */} +
+
+

+ Current Openings +

+ {vacancies.length > 0 ? ( +
+ {vacancies.map((vacancy) => ( + + ))} +
+ ) : ( +
+ +

+ No Open Vacancies Right Now +

+

+ 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! +

+
+ )} +
+
+ + {/* Section 3: Future Positions / CV Submission */} +
+
+ {" "} + {/* Ensure icon contrast */} +

+ {" "} + {/* Ensure heading contrast */} + Don't See the Right Fit? +

+

+ {" "} + {/* 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. +

+
+ {/* --- Updated Button --- */} + + {/* --- End Updated Button --- */} +
+
+
+ + {/* --- Modal for Future Position Application --- */} + + + +
+ ); +} diff --git a/components/ContactForm.tsx b/components/ContactForm.tsx index 2705599..a9dd6b2 100644 --- a/components/ContactForm.tsx +++ b/components/ContactForm.tsx @@ -1,40 +1,54 @@ "use client"; -import React, { useActionState, useEffect, useRef } from "react"; -import { useFormStatus } from "react-dom"; +import React, { useEffect, useActionState } from "react"; // Removed useRef +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 Button from "@/components/ui/Button"; // Use your existing Button component +import Button from "@/components/ui/Button"; -// Submit button component with pending state -function SubmitButton() { - const { pending } = useFormStatus(); - return ( - - ); -} +// Zod schema for contact form validation +const contactSchema = z.object({ + name: z.string().min(2, "Full name must be at least 2 characters"), + email: z.string().email("Invalid email address"), + subject: z.string().min(3, "Subject must be at least 3 characters"), + message: z.string().min(10, "Message must be at least 10 characters"), +}); + +type ContactFormData = z.infer; -// The main contact form component export default function ContactForm() { + // React Hook Form setup + const { + register, + formState: { errors, isValid }, + reset, + } = useForm({ + resolver: zodResolver(contactSchema), + mode: "onChange", + }); + const initialState: ContactFormState = { message: null, errors: {}, success: false, }; - const [state, dispatch] = useActionState(submitContactForm, initialState); - const formRef = useRef(null); // Ref to reset the form + const [state, formAction] = useActionState(submitContactForm, initialState); // Renamed dispatch to formAction for clarity - // Reset form on successful submission + // Reset form when server action reports success useEffect(() => { 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 ( -
- {/* Name Input */} + + {/* Name Field */}
- {/* Email Input */} + {/* Email Field */}
- {/* Subject Input */} + {/* Subject Field */}
- {/* Message Textarea */} + {/* Message Field */}
- - {/* General Form Message (Success or Error) */} -
- {state.message && ( + 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 ${ + errors.message + ? "border-destructive" + : "border-border dark:border-gray-600" + }`} + /> + {errors.message && ( )} + {state.errors?.message && + state.errors.message.map((err: string) => ( +

+ {err} +

+ ))}
+ {/* General Form Response */} + {state.message && ( +

+ {state.message} +

+ )} + {/* Submit Button */} + {/* isSubmitting from useFormState is now implicitly handled by the form action */}
- +
);