@notionhive/contacts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/bin/contacts.js +16 -0
  2. package/category.config.json +7 -0
  3. package/package.json +24 -0
  4. package/registry/contact-01.json +9 -0
  5. package/registry/contact-02.json +9 -0
  6. package/registry/contact-03.json +9 -0
  7. package/registry/contact-04.json +6 -0
  8. package/registry/contact-05.json +9 -0
  9. package/registry/contact-06.json +9 -0
  10. package/registry/contact-07.json +9 -0
  11. package/registry/contact-08.json +6 -0
  12. package/registry/contact-09.json +10 -0
  13. package/registry/contact-10.json +6 -0
  14. package/registry/index.json +88 -0
  15. package/templates/components/atoms/SafeImage/SafeImage.jsx +101 -0
  16. package/templates/components/atoms/SafeImage/index.js +1 -0
  17. package/templates/components/hooks/useCarousel.js +73 -0
  18. package/templates/components/organisms/Contact01/Contact01.jsx +363 -0
  19. package/templates/components/organisms/Contact01/Contact01.propTypes.js +105 -0
  20. package/templates/components/organisms/Contact01/index.js +1 -0
  21. package/templates/components/organisms/Contact02/Contact02.jsx +288 -0
  22. package/templates/components/organisms/Contact02/Contact02.propTypes.js +78 -0
  23. package/templates/components/organisms/Contact02/index.js +1 -0
  24. package/templates/components/organisms/Contact03/Contact03.jsx +94 -0
  25. package/templates/components/organisms/Contact03/Contact03.propTypes.js +25 -0
  26. package/templates/components/organisms/Contact03/index.js +1 -0
  27. package/templates/components/organisms/Contact04/Contact04.jsx +239 -0
  28. package/templates/components/organisms/Contact04/Contact04.propTypes.js +50 -0
  29. package/templates/components/organisms/Contact04/index.js +1 -0
  30. package/templates/components/organisms/Contact05/Contact05.jsx +272 -0
  31. package/templates/components/organisms/Contact05/Contact05.propTypes.js +49 -0
  32. package/templates/components/organisms/Contact05/index.js +1 -0
  33. package/templates/components/organisms/Contact06/Contact06.jsx +223 -0
  34. package/templates/components/organisms/Contact06/Contact06.propTypes.js +40 -0
  35. package/templates/components/organisms/Contact06/index.js +1 -0
  36. package/templates/components/organisms/Contact07/Contact07.jsx +348 -0
  37. package/templates/components/organisms/Contact07/Contact07.propTypes.js +92 -0
  38. package/templates/components/organisms/Contact07/index.js +1 -0
  39. package/templates/components/organisms/Contact08/Contact08.jsx +178 -0
  40. package/templates/components/organisms/Contact08/Contact08.propTypes.js +36 -0
  41. package/templates/components/organisms/Contact08/index.js +1 -0
  42. package/templates/components/organisms/Contact09/Contact09.jsx +345 -0
  43. package/templates/components/organisms/Contact09/Contact09.propTypes.js +96 -0
  44. package/templates/components/organisms/Contact09/index.js +1 -0
  45. package/templates/components/organisms/Contact10/Contact10.jsx +175 -0
  46. package/templates/components/organisms/Contact10/Contact10.propTypes.js +55 -0
  47. package/templates/components/organisms/Contact10/index.js +1 -0
  48. package/templates/public/contact/contact01/ellipse.svg +12 -0
  49. package/templates/public/contact/contact01/logo-accenture.svg +6 -0
  50. package/templates/public/contact/contact01/logo-amart.svg +12 -0
  51. package/templates/public/contact/contact01/logo-brand12.png +0 -0
  52. package/templates/public/contact/contact01/logo-brand5.png +0 -0
  53. package/templates/public/contact/contact01/logo-brand7.png +0 -0
  54. package/templates/public/contact/contact01/logo-brand9.svg +12 -0
  55. package/templates/public/contact/contact01/logo-great-eastern.png +0 -0
  56. package/templates/public/contact/contact01/logo-kenwood.png +0 -0
  57. package/templates/public/contact/contact01/logo-nissan.svg +12 -0
  58. package/templates/public/contact/contact01/logo-suncorp.png +0 -0
  59. package/templates/public/contact/contact01/logo-ticketmaster.png +0 -0
  60. package/templates/public/contact/contact01/logo-toyota.svg +5 -0
  61. package/templates/public/contact/contact02/map.jpg +0 -0
  62. package/templates/public/contact/contact03/bg.jpg +0 -0
  63. package/templates/public/contact/contact03/card-photo.jpg +0 -0
  64. package/templates/public/contact/contact05/hero.jpg +0 -0
  65. package/templates/public/contact/contact05/pattern.png +0 -0
  66. package/templates/public/contact/contact06/hero.jpg +0 -0
  67. package/templates/public/contact/contact07/hero.jpg +0 -0
  68. package/templates/public/contact/contact09/avatar.jpg +0 -0
@@ -0,0 +1,92 @@
1
+ import PropTypes from "prop-types";
2
+
3
+ const officeShape = PropTypes.shape({
4
+ name: PropTypes.string.isRequired,
5
+ email: PropTypes.string,
6
+ phone: PropTypes.string,
7
+ hours: PropTypes.string,
8
+ });
9
+
10
+ const faqItemShape = PropTypes.shape({
11
+ question: PropTypes.string.isRequired,
12
+ answer: PropTypes.string,
13
+ });
14
+
15
+ export const contact07PropTypes = {
16
+ headline: PropTypes.string,
17
+ description: PropTypes.string,
18
+ heroImage: PropTypes.string,
19
+ heroAlt: PropTypes.string,
20
+ offices: PropTypes.arrayOf(officeShape),
21
+ topicOptions: PropTypes.arrayOf(PropTypes.string),
22
+ submitText: PropTypes.string,
23
+ faqTitle: PropTypes.string,
24
+ faqItems: PropTypes.arrayOf(faqItemShape),
25
+ defaultOpenFaq: PropTypes.number,
26
+ activeFaq: PropTypes.number,
27
+ onFaqChange: PropTypes.func,
28
+ formData: PropTypes.shape({
29
+ fullName: PropTypes.string,
30
+ email: PropTypes.string,
31
+ phone: PropTypes.string,
32
+ orderId: PropTypes.string,
33
+ topic: PropTypes.string,
34
+ message: PropTypes.string,
35
+ }),
36
+ onFieldChange: PropTypes.func,
37
+ onSubmit: PropTypes.func,
38
+ showFaq: PropTypes.bool,
39
+ className: PropTypes.string,
40
+ };
41
+
42
+ export const contact07DefaultProps = {
43
+ headline: "Get in Touch",
44
+ description:
45
+ "Whether you have a question, need support, or simply want to share your feedback, the Gentle Park team is just a message away. Reach out to us through any of the channels below — we're committed to ensuring you have the best experience with Gentle Park.",
46
+ heroImage: "/contact/contact07/hero.jpg",
47
+ heroAlt: "Woman in flowing dress on sand dunes",
48
+ offices: [
49
+ {
50
+ name: "Bangladesh",
51
+ email: "support.bd@gentlepark.com",
52
+ phone: "+880 16 891 31186",
53
+ hours: "Sat–Thu, 9 AM – 8 PM (BST)",
54
+ },
55
+ {
56
+ name: "Dubai",
57
+ email: "support.uae@gentlepark.com",
58
+ phone: "+966 12 206 7346",
59
+ hours: "Sun–Thu, 10 AM – 9 PM (GST)",
60
+ },
61
+ ],
62
+ topicOptions: [
63
+ "Order inquiry",
64
+ "Returns & exchanges",
65
+ "Product question",
66
+ "General feedback",
67
+ ],
68
+ submitText: "Send Message",
69
+ faqTitle: "FAQs",
70
+ faqItems: [
71
+ {
72
+ question: "How to place an order in an online store?",
73
+ answer:
74
+ "To place an order in our online store, simply browse our products and add your desired items to the cart. Once you're ready, go to your cart, review your selections, and proceed to checkout. Enter your shipping details, choose a payment method, and confirm your order. After completing the payment, you'll receive a confirmation email with your order details.",
75
+ },
76
+ { question: "How much does the delivery cost?", answer: "" },
77
+ { question: "What are the delivery methods?", answer: "" },
78
+ {
79
+ question: "How do I find out the availability of goods in the store?",
80
+ answer: "",
81
+ },
82
+ { question: "How to exchange or return the goods?", answer: "" },
83
+ ],
84
+ defaultOpenFaq: 0,
85
+ showFaq: true,
86
+ formData: undefined,
87
+ onFieldChange: undefined,
88
+ onSubmit: undefined,
89
+ onFaqChange: undefined,
90
+ activeFaq: undefined,
91
+ className: "",
92
+ };
@@ -0,0 +1 @@
1
+ export { Contact07, default } from "./Contact07";
@@ -0,0 +1,178 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ contact08DefaultProps,
6
+ contact08PropTypes,
7
+ } from "./Contact08.propTypes";
8
+
9
+ const inputClassName =
10
+ "h-[52px] w-full rounded-xl border border-[#e1e2e3] bg-white px-5 text-base text-[#1b1e2e] outline-none transition-colors duration-200 ease-out placeholder:text-[#7e8088] focus:border-[#283aff] focus:ring-1 focus:ring-[#283aff]/20";
11
+
12
+ function ChevronDownIcon({ className = "" }) {
13
+ return (
14
+ <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className={className}>
15
+ <path
16
+ d="M5 8l5 5 5-5"
17
+ stroke="currentColor"
18
+ strokeWidth="1.5"
19
+ strokeLinecap="round"
20
+ strokeLinejoin="round"
21
+ />
22
+ </svg>
23
+ );
24
+ }
25
+
26
+ function FieldLabel({ children, required = false }) {
27
+ return (
28
+ <label className="text-sm font-medium tracking-[0.28px] text-[#343744]">
29
+ {children}
30
+ {required ? <span className="text-[#ff373c]"> *</span> : null}
31
+ </label>
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Contact08 — Immigration inquiry form with headline copy and bordered inputs.
37
+ * Figma node 26:17291 (form section only) at 1920px.
38
+ *
39
+ * @param {object} props - See Contact08.propTypes.js.
40
+ */
41
+ export function Contact08({
42
+ headline = contact08DefaultProps.headline,
43
+ description = contact08DefaultProps.description,
44
+ immigrationGoalOptions = contact08DefaultProps.immigrationGoalOptions,
45
+ submitText = contact08DefaultProps.submitText,
46
+ formData: controlledForm,
47
+ onFieldChange,
48
+ onSubmit,
49
+ className = "",
50
+ }) {
51
+ const [internalForm, setInternalForm] = useState({
52
+ fullName: "",
53
+ phone: "",
54
+ email: "",
55
+ immigrationGoal: "",
56
+ message: "",
57
+ });
58
+
59
+ const form = controlledForm ?? internalForm;
60
+
61
+ const setField = (field, value) => {
62
+ onFieldChange?.(field, value);
63
+ if (!controlledForm) {
64
+ setInternalForm((prev) => ({ ...prev, [field]: value }));
65
+ }
66
+ };
67
+
68
+ const handleSubmit = (event) => {
69
+ event.preventDefault();
70
+ onSubmit?.(form);
71
+ };
72
+
73
+ return (
74
+ <section
75
+ className={["relative w-full overflow-hidden bg-white", className]
76
+ .filter(Boolean)
77
+ .join(" ")}
78
+ data-contact="contact08"
79
+ >
80
+ <div className="relative mx-auto w-full max-w-[1920px] px-4 py-12 sm:px-6 md:px-10 lg:px-20 lg:py-20 xl:px-[160px] xl:py-24">
81
+ <div className="mx-auto flex w-full max-w-[1330px] flex-col gap-12 lg:flex-row lg:items-start lg:justify-between lg:gap-16 xl:gap-24">
82
+ {/* Left — copy */}
83
+ <div className="flex w-full max-w-[520px] flex-col gap-6 lg:sticky lg:top-8">
84
+ <h2 className="text-3xl font-semibold leading-[1.1] tracking-[-0.02em] text-[#020617] sm:text-4xl xl:text-[48px]">
85
+ {headline}
86
+ </h2>
87
+ <p className="text-base leading-[1.5] text-[#343744] sm:text-lg">
88
+ {description}
89
+ </p>
90
+ </div>
91
+
92
+ {/* Right — form */}
93
+ <form
94
+ onSubmit={handleSubmit}
95
+ className="flex w-full max-w-[600px] flex-col gap-5"
96
+ >
97
+ <div className="flex flex-col gap-2">
98
+ <FieldLabel required>Full Name</FieldLabel>
99
+ <input
100
+ type="text"
101
+ value={form.fullName}
102
+ onChange={(e) => setField("fullName", e.target.value)}
103
+ className={inputClassName}
104
+ required
105
+ />
106
+ </div>
107
+
108
+ <div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
109
+ <div className="flex flex-col gap-2">
110
+ <FieldLabel required>Phone</FieldLabel>
111
+ <input
112
+ type="tel"
113
+ value={form.phone}
114
+ onChange={(e) => setField("phone", e.target.value)}
115
+ className={inputClassName}
116
+ required
117
+ />
118
+ </div>
119
+ <div className="flex flex-col gap-2">
120
+ <FieldLabel required>Email</FieldLabel>
121
+ <input
122
+ type="email"
123
+ value={form.email}
124
+ onChange={(e) => setField("email", e.target.value)}
125
+ className={inputClassName}
126
+ required
127
+ />
128
+ </div>
129
+ </div>
130
+
131
+ <div className="flex flex-col gap-2">
132
+ <FieldLabel required>Immigration Goal</FieldLabel>
133
+ <div className="relative">
134
+ <select
135
+ value={form.immigrationGoal}
136
+ onChange={(e) => setField("immigrationGoal", e.target.value)}
137
+ className={`${inputClassName} appearance-none pr-12`}
138
+ required
139
+ >
140
+ <option value="" disabled>
141
+ Select your goal
142
+ </option>
143
+ {immigrationGoalOptions.map((option) => (
144
+ <option key={option} value={option}>
145
+ {option}
146
+ </option>
147
+ ))}
148
+ </select>
149
+ <ChevronDownIcon className="pointer-events-none absolute right-5 top-1/2 size-5 -translate-y-1/2 text-[#7e8088]" />
150
+ </div>
151
+ </div>
152
+
153
+ <div className="flex flex-col gap-2">
154
+ <FieldLabel>Message</FieldLabel>
155
+ <textarea
156
+ value={form.message}
157
+ onChange={(e) => setField("message", e.target.value)}
158
+ rows={5}
159
+ className="min-h-[140px] w-full resize-none rounded-xl border border-[#e1e2e3] bg-white px-5 py-3.5 text-base text-[#1b1e2e] outline-none transition-colors duration-200 ease-out placeholder:text-[#7e8088] focus:border-[#283aff] focus:ring-1 focus:ring-[#283aff]/20"
160
+ />
161
+ </div>
162
+
163
+ <button
164
+ type="submit"
165
+ className="mt-2 flex h-14 w-full items-center justify-center rounded-xl bg-[#283aff] text-base font-semibold tracking-[0.32px] text-white transition-colors duration-200 ease-out hover:bg-[#1f2ecc] focus-visible:outline-2 focus-visible:outline-offset-2 sm:w-max sm:min-w-[200px] sm:px-10"
166
+ >
167
+ {submitText}
168
+ </button>
169
+ </form>
170
+ </div>
171
+ </div>
172
+ </section>
173
+ );
174
+ }
175
+
176
+ Contact08.propTypes = contact08PropTypes;
177
+
178
+ export default Contact08;
@@ -0,0 +1,36 @@
1
+ import PropTypes from "prop-types";
2
+
3
+ export const contact08PropTypes = {
4
+ headline: PropTypes.string,
5
+ description: PropTypes.string,
6
+ immigrationGoalOptions: PropTypes.arrayOf(PropTypes.string),
7
+ submitText: PropTypes.string,
8
+ formData: PropTypes.shape({
9
+ fullName: PropTypes.string,
10
+ phone: PropTypes.string,
11
+ email: PropTypes.string,
12
+ immigrationGoal: PropTypes.string,
13
+ message: PropTypes.string,
14
+ }),
15
+ onFieldChange: PropTypes.func,
16
+ onSubmit: PropTypes.func,
17
+ className: PropTypes.string,
18
+ };
19
+
20
+ export const contact08DefaultProps = {
21
+ headline: "Start Your Immigration Journey",
22
+ description:
23
+ "Our licensed consultants are ready to guide you through every step — from initial assessment to final approval. Share your details and we'll be in touch within 24 hours.",
24
+ immigrationGoalOptions: [
25
+ "Work visa",
26
+ "Student visa",
27
+ "Permanent residency",
28
+ "Family sponsorship",
29
+ "Citizenship application",
30
+ ],
31
+ submitText: "Submit Inquiry",
32
+ formData: undefined,
33
+ onFieldChange: undefined,
34
+ onSubmit: undefined,
35
+ className: "",
36
+ };
@@ -0,0 +1 @@
1
+ export { Contact08, default } from "./Contact08";
@@ -0,0 +1,345 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import SafeImage from "../../atoms/SafeImage";
5
+ import { useCarousel } from "../../hooks/useCarousel";
6
+ import {
7
+ contact09DefaultProps,
8
+ contact09PropTypes,
9
+ } from "./Contact09.propTypes";
10
+
11
+ const underlineInputClass =
12
+ "w-full bg-transparent text-base text-white outline-none transition-colors duration-200 ease-out placeholder:text-white/40";
13
+
14
+ function ChevronDownIcon({ className = "" }) {
15
+ return (
16
+ <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className={className}>
17
+ <path
18
+ d="M5 8l5 5 5-5"
19
+ stroke="currentColor"
20
+ strokeWidth="1.5"
21
+ strokeLinecap="round"
22
+ strokeLinejoin="round"
23
+ />
24
+ </svg>
25
+ );
26
+ }
27
+
28
+ function ArrowRightIcon({ className = "" }) {
29
+ return (
30
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
31
+ <path
32
+ d="M5 12h14M13 6l6 6-6 6"
33
+ stroke="currentColor"
34
+ strokeWidth="1.5"
35
+ strokeLinecap="round"
36
+ strokeLinejoin="round"
37
+ />
38
+ </svg>
39
+ );
40
+ }
41
+
42
+ function QuoteIcon({ className = "" }) {
43
+ return (
44
+ <svg viewBox="0 0 56 40" fill="none" aria-hidden="true" className={className}>
45
+ <path
46
+ d="M0 40V23C0 14 2.667 7 8 2.5 13.333-2 20-2 28 2L24 11C20 8.667 16.667 8.667 13.333 11 10 13.333 9 17 9 22V40H0zm28 0V23c0-9 2.667-16 8-20.5C41.333-2 48-2 56 2L52 11C48 8.667 44.667 8.667 41.333 11 38 13.333 37 17 37 22V40H28z"
47
+ fill="#e6232d"
48
+ />
49
+ </svg>
50
+ );
51
+ }
52
+
53
+ function ChevronLeftIcon({ className = "" }) {
54
+ return (
55
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
56
+ <path
57
+ d="M15 6l-6 6 6 6"
58
+ stroke="currentColor"
59
+ strokeWidth="1.5"
60
+ strokeLinecap="round"
61
+ strokeLinejoin="round"
62
+ />
63
+ </svg>
64
+ );
65
+ }
66
+
67
+ function ChevronRightIcon({ className = "" }) {
68
+ return (
69
+ <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
70
+ <path
71
+ d="M9 6l6 6-6 6"
72
+ stroke="currentColor"
73
+ strokeWidth="1.5"
74
+ strokeLinecap="round"
75
+ strokeLinejoin="round"
76
+ />
77
+ </svg>
78
+ );
79
+ }
80
+
81
+ function UnderlineField({ label, required = false, children }) {
82
+ return (
83
+ <div className="flex flex-col gap-1 border-b border-white/20 py-4">
84
+ <label className="text-xs font-medium uppercase tracking-[0.96px] text-white/60">
85
+ {label}
86
+ {required ? "*" : ""}
87
+ </label>
88
+ {children}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ function formatSlideCounter(index, total) {
94
+ const current = String(index + 1).padStart(2, "0");
95
+ const max = String(total).padStart(2, "0");
96
+ return `${current}/${max}`;
97
+ }
98
+
99
+ /**
100
+ * Contact09 — Suvastu dark contact form with testimonial carousel card.
101
+ * Figma node 26:17334 at 1920px.
102
+ *
103
+ * @param {object} props - See Contact09.propTypes.js.
104
+ */
105
+ export function Contact09({
106
+ headline = contact09DefaultProps.headline,
107
+ description = contact09DefaultProps.description,
108
+ serviceOptions = contact09DefaultProps.serviceOptions,
109
+ submitText = contact09DefaultProps.submitText,
110
+ testimonials = contact09DefaultProps.testimonials,
111
+ activeSlide: activeSlideProp = contact09DefaultProps.activeSlide,
112
+ onSlideChange,
113
+ formData: controlledForm,
114
+ onFieldChange,
115
+ onSubmit,
116
+ className = "",
117
+ }) {
118
+ const [internalForm, setInternalForm] = useState({
119
+ fullName: "",
120
+ email: "",
121
+ phone: "",
122
+ serviceType: "",
123
+ message: "",
124
+ });
125
+
126
+ const isControlledSlide = typeof activeSlideProp === "number";
127
+ const { activeSlide, goTo, next, prev } = useCarousel({
128
+ count: testimonials.length,
129
+ initialIndex: activeSlideProp,
130
+ onChange: onSlideChange,
131
+ });
132
+ const currentSlide = isControlledSlide ? activeSlideProp : activeSlide;
133
+ const activeTestimonial = testimonials[currentSlide] ?? testimonials[0];
134
+
135
+ const form = controlledForm ?? internalForm;
136
+
137
+ const setField = (field, value) => {
138
+ onFieldChange?.(field, value);
139
+ if (!controlledForm) {
140
+ setInternalForm((prev) => ({ ...prev, [field]: value }));
141
+ }
142
+ };
143
+
144
+ const handleSubmit = (event) => {
145
+ event.preventDefault();
146
+ onSubmit?.(form);
147
+ };
148
+
149
+ const handlePrev = () => {
150
+ if (isControlledSlide) {
151
+ const nextIndex = Math.max(0, activeSlideProp - 1);
152
+ onSlideChange?.(nextIndex);
153
+ } else {
154
+ prev();
155
+ }
156
+ };
157
+
158
+ const handleNext = () => {
159
+ if (isControlledSlide) {
160
+ const nextIndex = Math.min(testimonials.length - 1, activeSlideProp + 1);
161
+ onSlideChange?.(nextIndex);
162
+ } else {
163
+ next();
164
+ }
165
+ };
166
+
167
+ useEffect(() => {
168
+ if (!isControlledSlide) {
169
+ goTo(activeSlideProp);
170
+ }
171
+ }, [activeSlideProp, goTo, isControlledSlide]);
172
+
173
+ return (
174
+ <section
175
+ className={["relative w-full overflow-hidden bg-[#111a47]", className]
176
+ .filter(Boolean)
177
+ .join(" ")}
178
+ data-contact="contact09"
179
+ >
180
+ <div className="relative mx-auto w-full max-w-[1920px] px-4 py-12 sm:px-6 md:px-10 lg:px-20 lg:py-20 xl:px-[160px] xl:py-24">
181
+ <div className="mx-auto flex w-full max-w-[1330px] flex-col gap-12 lg:flex-row lg:items-stretch lg:gap-16 xl:gap-20">
182
+ {/* Left — form */}
183
+ <form
184
+ onSubmit={handleSubmit}
185
+ className="flex w-full flex-col gap-8 lg:w-1/2 lg:max-w-[600px]"
186
+ >
187
+ <div className="flex flex-col gap-4 text-white">
188
+ <h2 className="text-3xl font-semibold leading-[1.1] tracking-[-0.02em] sm:text-4xl xl:text-[48px]">
189
+ {headline}
190
+ </h2>
191
+ <p className="text-base leading-[1.5] text-white/75 sm:text-lg">
192
+ {description}
193
+ </p>
194
+ </div>
195
+
196
+ <div className="flex flex-col">
197
+ <UnderlineField label="Full Name" required>
198
+ <input
199
+ type="text"
200
+ value={form.fullName}
201
+ onChange={(e) => setField("fullName", e.target.value)}
202
+ className={underlineInputClass}
203
+ required
204
+ />
205
+ </UnderlineField>
206
+
207
+ <UnderlineField label="Email" required>
208
+ <input
209
+ type="email"
210
+ value={form.email}
211
+ onChange={(e) => setField("email", e.target.value)}
212
+ className={underlineInputClass}
213
+ required
214
+ />
215
+ </UnderlineField>
216
+
217
+ <UnderlineField label="Phone">
218
+ <input
219
+ type="tel"
220
+ value={form.phone}
221
+ onChange={(e) => setField("phone", e.target.value)}
222
+ className={underlineInputClass}
223
+ />
224
+ </UnderlineField>
225
+
226
+ <div className="relative flex flex-col gap-1 border-b border-white/20 py-4">
227
+ <label className="text-xs font-medium uppercase tracking-[0.96px] text-white/60">
228
+ Service Type
229
+ </label>
230
+ <select
231
+ value={form.serviceType}
232
+ onChange={(e) => setField("serviceType", e.target.value)}
233
+ className={`${underlineInputClass} appearance-none pr-8`}
234
+ >
235
+ <option value="" disabled className="text-[#111a47]">
236
+ Select service
237
+ </option>
238
+ {serviceOptions.map((option) => (
239
+ <option key={option} value={option} className="text-[#111a47]">
240
+ {option}
241
+ </option>
242
+ ))}
243
+ </select>
244
+ <ChevronDownIcon className="pointer-events-none absolute bottom-5 right-0 size-5 text-white/60" />
245
+ </div>
246
+
247
+ <div className="flex flex-col gap-1 border-b border-white/20 py-4">
248
+ <label className="text-xs font-medium uppercase tracking-[0.96px] text-white/60">
249
+ Message
250
+ </label>
251
+ <textarea
252
+ value={form.message}
253
+ onChange={(e) => setField("message", e.target.value)}
254
+ rows={3}
255
+ className={`${underlineInputClass} min-h-[60px] resize-none`}
256
+ />
257
+ </div>
258
+ </div>
259
+
260
+ <button
261
+ type="submit"
262
+ className="flex h-14 w-max max-w-full items-center gap-3 rounded-full bg-white px-8 text-base font-semibold tracking-[0.32px] text-[#111a47] transition-colors duration-200 ease-out hover:bg-white/90 focus-visible:outline-2 focus-visible:outline-offset-2"
263
+ >
264
+ {submitText}
265
+ <ArrowRightIcon className="size-5" />
266
+ </button>
267
+ </form>
268
+
269
+ {/* Right — testimonial card */}
270
+ <div className="flex w-full lg:w-1/2 lg:items-center">
271
+ <article className="flex w-full flex-col gap-8 rounded-2xl bg-white p-6 sm:p-8 lg:p-10">
272
+ <QuoteIcon className="size-10 shrink-0" />
273
+
274
+ <div className="overflow-hidden">
275
+ <div
276
+ className="flex motion-reduce:transition-none transition-transform duration-500 ease-out"
277
+ style={{ transform: `translateX(-${currentSlide * 100}%)` }}
278
+ >
279
+ {testimonials.map((item) => (
280
+ <blockquote
281
+ key={item.id ?? item.authorName}
282
+ className="w-full shrink-0 text-lg leading-[1.5] text-[#1b1e2e] sm:text-xl lg:text-2xl"
283
+ >
284
+ {item.quote}
285
+ </blockquote>
286
+ ))}
287
+ </div>
288
+ </div>
289
+
290
+ <div className="flex items-center gap-4">
291
+ <div className="relative size-14 shrink-0 overflow-hidden rounded-full">
292
+ <SafeImage
293
+ src={activeTestimonial?.avatarSrc ?? "/contact/contact09/avatar.jpg"}
294
+ alt={activeTestimonial?.avatarAlt ?? activeTestimonial?.authorName ?? ""}
295
+ fill
296
+ className="object-cover object-center"
297
+ sizes="56px"
298
+ />
299
+ </div>
300
+ <div className="flex min-w-0 flex-col gap-0.5">
301
+ <p className="text-base font-semibold text-[#020617] sm:text-lg">
302
+ {activeTestimonial?.authorName}
303
+ </p>
304
+ {activeTestimonial?.authorTitle ? (
305
+ <p className="text-sm text-[#7e8088]">{activeTestimonial.authorTitle}</p>
306
+ ) : null}
307
+ </div>
308
+ </div>
309
+
310
+ <div className="flex items-center justify-between gap-4 border-t border-[#e1e2e3] pt-6">
311
+ <span className="text-sm font-medium tracking-[0.28px] text-[#7e8088]">
312
+ {formatSlideCounter(currentSlide, testimonials.length)}
313
+ </span>
314
+ <div className="flex items-center gap-2">
315
+ <button
316
+ type="button"
317
+ onClick={handlePrev}
318
+ disabled={currentSlide === 0}
319
+ aria-label="Previous testimonial"
320
+ className="flex size-10 items-center justify-center rounded-full border border-[#e1e2e3] text-[#020617] transition-colors duration-200 ease-out hover:bg-[#f9f9fa] focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-40"
321
+ >
322
+ <ChevronLeftIcon className="size-5" />
323
+ </button>
324
+ <button
325
+ type="button"
326
+ onClick={handleNext}
327
+ disabled={currentSlide >= testimonials.length - 1}
328
+ aria-label="Next testimonial"
329
+ className="flex size-10 items-center justify-center rounded-full border border-[#e1e2e3] text-[#020617] transition-colors duration-200 ease-out hover:bg-[#f9f9fa] focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-40"
330
+ >
331
+ <ChevronRightIcon className="size-5" />
332
+ </button>
333
+ </div>
334
+ </div>
335
+ </article>
336
+ </div>
337
+ </div>
338
+ </div>
339
+ </section>
340
+ );
341
+ }
342
+
343
+ Contact09.propTypes = contact09PropTypes;
344
+
345
+ export default Contact09;