contact page
This commit is contained in:
6
app/contact/page.tsx
Normal file
6
app/contact/page.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Contact } from "@/components/pages/contact";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return <Contact />;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
AboutUs,
|
AboutUs,
|
||||||
Banner,
|
Banner,
|
||||||
|
Blog,
|
||||||
Line,
|
Line,
|
||||||
OurService,
|
OurService,
|
||||||
Statistics,
|
Statistics,
|
||||||
@@ -10,7 +11,7 @@ import {
|
|||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<main className="bg-slate-950 mb-90">
|
<main className="bg-slate-950">
|
||||||
<Banner />
|
<Banner />
|
||||||
<Statistics />
|
<Statistics />
|
||||||
<AboutUs />
|
<AboutUs />
|
||||||
@@ -18,6 +19,7 @@ export default function Home() {
|
|||||||
<OurService />
|
<OurService />
|
||||||
<Testimonial />
|
<Testimonial />
|
||||||
<Line />
|
<Line />
|
||||||
|
<Blog />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
24
components/pages/contact/contactHeader.tsx
Normal file
24
components/pages/contact/contactHeader.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function ContactHeader() {
|
||||||
|
return (
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<div className="mb-4 flex items-center justify-center gap-2">
|
||||||
|
<DotAnimatsiya />
|
||||||
|
<span className="text-sm font-semibold tracking-wider text-white">
|
||||||
|
CONTACT US
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2
|
||||||
|
className="bg-linear-to-br from-white via-white to-black
|
||||||
|
text-transparent bg-clip-text text-4xl font-bold tracking-wide md:text-5xl"
|
||||||
|
>
|
||||||
|
GET IN TOUCH
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 text-sm text-gray-300 italic">
|
||||||
|
We'd love to hear from you. Please fill out this form.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
components/pages/contact/index.ts
Normal file
1
components/pages/contact/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { Contact } from "./main";
|
||||||
319
components/pages/contact/main.tsx
Normal file
319
components/pages/contact/main.tsx
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Mail, MapPin, Phone, Check } from "lucide-react";
|
||||||
|
import ContactHeader from "./contactHeader";
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
subject: string;
|
||||||
|
message: string;
|
||||||
|
agreeToPolicy: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormErrors {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
email?: string;
|
||||||
|
subject?: string;
|
||||||
|
message?: string;
|
||||||
|
agreeToPolicy?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Contact() {
|
||||||
|
const [formData, setFormData] = useState<FormData>({
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
email: "",
|
||||||
|
subject: "",
|
||||||
|
message: "",
|
||||||
|
agreeToPolicy: false,
|
||||||
|
});
|
||||||
|
const [errors, setErrors] = useState<FormErrors>({});
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [submitStatus, setSubmitStatus] = useState<
|
||||||
|
"idle" | "success" | "error"
|
||||||
|
>("idle");
|
||||||
|
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
const newErrors: FormErrors = {};
|
||||||
|
|
||||||
|
if (!formData.firstName.trim()) {
|
||||||
|
newErrors.firstName = "First name is required";
|
||||||
|
}
|
||||||
|
if (!formData.lastName.trim()) {
|
||||||
|
newErrors.lastName = "Last name is required";
|
||||||
|
}
|
||||||
|
if (!formData.email.trim()) {
|
||||||
|
newErrors.email = "Email is required";
|
||||||
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||||
|
newErrors.email = "Please enter a valid email";
|
||||||
|
}
|
||||||
|
if (!formData.subject.trim()) {
|
||||||
|
newErrors.subject = "Subject is required";
|
||||||
|
}
|
||||||
|
if (!formData.message.trim()) {
|
||||||
|
newErrors.message = "Message is required";
|
||||||
|
}
|
||||||
|
if (!formData.agreeToPolicy) {
|
||||||
|
newErrors.agreeToPolicy = "You must agree to the privacy policy";
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
|
) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
|
if (errors[name as keyof FormErrors]) {
|
||||||
|
setErrors((prev) => ({ ...prev, [name]: undefined }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckboxChange = () => {
|
||||||
|
setFormData((prev) => ({ ...prev, agreeToPolicy: !prev.agreeToPolicy }));
|
||||||
|
if (errors.agreeToPolicy) {
|
||||||
|
setErrors((prev) => ({ ...prev, agreeToPolicy: undefined }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!validateForm()) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setSubmitStatus("idle");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/contact", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setSubmitStatus("success");
|
||||||
|
setFormData({
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
email: "",
|
||||||
|
subject: "",
|
||||||
|
message: "",
|
||||||
|
agreeToPolicy: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSubmitStatus("error");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setSubmitStatus("error");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const contactInfo = [
|
||||||
|
{
|
||||||
|
icon: Mail,
|
||||||
|
title: "EMAIL",
|
||||||
|
detail: "support@fireforce",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: MapPin,
|
||||||
|
title: "OUR LOCATION",
|
||||||
|
detail: "Jl. Dr. Ir. Soekarno No. 99x Tabanan - Bali",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Phone,
|
||||||
|
title: "PHONE",
|
||||||
|
detail: "+123-456-7890",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="relative min-h-175 w-full py-16 md:py-40">
|
||||||
|
{/* Background Image */}
|
||||||
|
<div className="absolute inset-0 z-0">
|
||||||
|
<Image
|
||||||
|
src="/images/contactBanner.jpg"
|
||||||
|
alt="Contact background"
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
{/* Radial Gradient Overlay */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"radial-gradient(ellipse at bottom center, #d2610ab0 0%, #1e1d1ce3 70%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="relative z-10 mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<ContactHeader />
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{/* First Row - Names */}
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="firstName"
|
||||||
|
placeholder="First Name"
|
||||||
|
value={formData.firstName}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={`w-full rounded-md border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||||
|
errors.firstName ? "border-red-500" : "border-transparent"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{errors.firstName && (
|
||||||
|
<p className="mt-1 text-xs text-red-500">{errors.firstName}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="lastName"
|
||||||
|
placeholder="Last Name"
|
||||||
|
value={formData.lastName}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={`w-full rounded-md border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||||
|
errors.lastName ? "border-red-500" : "border-transparent"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{errors.lastName && (
|
||||||
|
<p className="mt-1 text-xs text-red-500">{errors.lastName}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Second Row - Email & Subject */}
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="Your Email Address"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={`w-full rounded-md border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||||
|
errors.email ? "border-red-500" : "border-transparent"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="mt-1 text-xs text-red-500">{errors.email}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="subject"
|
||||||
|
placeholder="Subject"
|
||||||
|
value={formData.subject}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={`w-full rounded-md border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||||
|
errors.subject ? "border-red-500" : "border-transparent"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{errors.subject && (
|
||||||
|
<p className="mt-1 text-xs text-red-500">{errors.subject}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message Textarea */}
|
||||||
|
<div>
|
||||||
|
<textarea
|
||||||
|
name="message"
|
||||||
|
placeholder="Leave us a message"
|
||||||
|
rows={5}
|
||||||
|
value={formData.message}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={`w-full resize-none rounded-md border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||||
|
errors.message ? "border-red-500" : "border-transparent"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{errors.message && (
|
||||||
|
<p className="mt-1 text-xs text-red-500">{errors.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Checkbox and Submit */}
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCheckboxChange}
|
||||||
|
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
|
||||||
|
formData.agreeToPolicy
|
||||||
|
? "border-red-600 bg-red-600"
|
||||||
|
: "border-gray-400 bg-transparent"
|
||||||
|
}`}
|
||||||
|
aria-label="Agree to privacy policy"
|
||||||
|
>
|
||||||
|
{formData.agreeToPolicy && (
|
||||||
|
<Check className="h-3 w-3 text-white" strokeWidth={3} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<span className="text-sm text-gray-300">
|
||||||
|
You agree to our friendly privacy policy
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="rounded-full bg-red-600 px-8 py-3 text-sm font-semibold uppercase tracking-wider text-white transition hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Sending..." : "Send Message"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{errors.agreeToPolicy && (
|
||||||
|
<p className="text-xs text-red-500">{errors.agreeToPolicy}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Status Messages */}
|
||||||
|
{submitStatus === "success" && (
|
||||||
|
<p className="text-center text-sm text-green-400">
|
||||||
|
Message sent successfully!
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{submitStatus === "error" && (
|
||||||
|
<p className="text-center text-sm text-red-400">
|
||||||
|
Failed to send message. Please try again.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Contact Info */}
|
||||||
|
<div className="mt-12 grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||||
|
{contactInfo.map((info) => (
|
||||||
|
<div
|
||||||
|
key={info.title}
|
||||||
|
className="flex flex-col items-center text-center"
|
||||||
|
>
|
||||||
|
<div className="mb-3 flex h-14 w-14 items-center justify-center rounded-full border-2 border-red-600">
|
||||||
|
<info.icon className="h-6 w-6 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-sm font-bold tracking-wider text-white">
|
||||||
|
{info.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-400">{info.detail}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
components/pages/home/blog.tsx
Normal file
97
components/pages/home/blog.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import { ChevronRight } from "lucide-react";
|
||||||
|
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
||||||
|
|
||||||
|
const blogPosts = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
image: "/images/home/videoSlide1.jpg",
|
||||||
|
category: "Tips & Trick",
|
||||||
|
title: "BEHIND THE HELMET: LIFE AS A FIREFIGHTER",
|
||||||
|
author: "John Doe",
|
||||||
|
date: "July 24, 2025",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
image: "/images/home/videoSlide2.jpg",
|
||||||
|
category: "Insight",
|
||||||
|
title: "FIREFIGHTING EQUIPMENT: TOOLS OF THE TRADE",
|
||||||
|
author: "John Doe",
|
||||||
|
date: "July 24, 2025",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
image: "/images/home/videoSlide4.jpg",
|
||||||
|
category: "News",
|
||||||
|
title: "FIREFIGHTER TRAINING TAKES TO BECOME A HERO",
|
||||||
|
author: "John Doe",
|
||||||
|
date: "July 24, 2025",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Blog() {
|
||||||
|
return (
|
||||||
|
<section className="bg-[#1f1f1f] py-45">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-12 text-center">
|
||||||
|
<div className="mb-4 flex items-center justify-center gap-2">
|
||||||
|
<DotAnimatsiya />
|
||||||
|
<span className="text-sm font-semibold tracking-wider text-white uppercase">
|
||||||
|
Blog & Articles
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2
|
||||||
|
className="bg-linear-to-br from-white via-white to-black
|
||||||
|
text-transparent bg-clip-text text-4xl font-bold tracking-tight md:text-5xl lg:text-6xl"
|
||||||
|
>
|
||||||
|
LATEST BLOG & NEWS
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Blog Cards Grid */}
|
||||||
|
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3 max-sm:place-items-center">
|
||||||
|
{blogPosts.map((post) => (
|
||||||
|
<article key={post.id} className="group">
|
||||||
|
{/* Image Container */}
|
||||||
|
<div className="relative mb-6 aspect-4/2 md:aspect-4/3 overflow-hidden rounded-lg">
|
||||||
|
<Image
|
||||||
|
src={post.image || "/placeholder.svg"}
|
||||||
|
alt={post.title}
|
||||||
|
fill
|
||||||
|
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
{/* Category Badge */}
|
||||||
|
<div className="absolute bottom-4 left-4">
|
||||||
|
<span className="rounded bg-red-600 px-4 py-2 text-sm font-medium text-white">
|
||||||
|
{post.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-3 text-lg font-bold leading-tight tracking-wide text-white uppercase md:text-xl">
|
||||||
|
{post.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mb-4 text-sm text-gray-400">
|
||||||
|
<span className="text-gray-500">by </span>
|
||||||
|
<span className="text-white">{post.author}</span>
|
||||||
|
<span className="mx-2 text-gray-500">•</span>
|
||||||
|
<span className="text-gray-400">{post.date}</span>
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="inline-flex items-center gap-1 text-sm font-semibold tracking-wider text-red-600 uppercase transition-colors hover:text-red-500"
|
||||||
|
>
|
||||||
|
Read More
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,3 +5,4 @@ export { Video } from "./video";
|
|||||||
export { OurService } from "./ourService";
|
export { OurService } from "./ourService";
|
||||||
export { Testimonial } from "./testimonal";
|
export { Testimonial } from "./testimonal";
|
||||||
export { Line } from "./line";
|
export { Line } from "./line";
|
||||||
|
export { Blog } from "./blog";
|
||||||
|
|||||||
Reference in New Issue
Block a user