@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.
- package/bin/contacts.js +16 -0
- package/category.config.json +7 -0
- package/package.json +24 -0
- package/registry/contact-01.json +9 -0
- package/registry/contact-02.json +9 -0
- package/registry/contact-03.json +9 -0
- package/registry/contact-04.json +6 -0
- package/registry/contact-05.json +9 -0
- package/registry/contact-06.json +9 -0
- package/registry/contact-07.json +9 -0
- package/registry/contact-08.json +6 -0
- package/registry/contact-09.json +10 -0
- package/registry/contact-10.json +6 -0
- package/registry/index.json +88 -0
- package/templates/components/atoms/SafeImage/SafeImage.jsx +101 -0
- package/templates/components/atoms/SafeImage/index.js +1 -0
- package/templates/components/hooks/useCarousel.js +73 -0
- package/templates/components/organisms/Contact01/Contact01.jsx +363 -0
- package/templates/components/organisms/Contact01/Contact01.propTypes.js +105 -0
- package/templates/components/organisms/Contact01/index.js +1 -0
- package/templates/components/organisms/Contact02/Contact02.jsx +288 -0
- package/templates/components/organisms/Contact02/Contact02.propTypes.js +78 -0
- package/templates/components/organisms/Contact02/index.js +1 -0
- package/templates/components/organisms/Contact03/Contact03.jsx +94 -0
- package/templates/components/organisms/Contact03/Contact03.propTypes.js +25 -0
- package/templates/components/organisms/Contact03/index.js +1 -0
- package/templates/components/organisms/Contact04/Contact04.jsx +239 -0
- package/templates/components/organisms/Contact04/Contact04.propTypes.js +50 -0
- package/templates/components/organisms/Contact04/index.js +1 -0
- package/templates/components/organisms/Contact05/Contact05.jsx +272 -0
- package/templates/components/organisms/Contact05/Contact05.propTypes.js +49 -0
- package/templates/components/organisms/Contact05/index.js +1 -0
- package/templates/components/organisms/Contact06/Contact06.jsx +223 -0
- package/templates/components/organisms/Contact06/Contact06.propTypes.js +40 -0
- package/templates/components/organisms/Contact06/index.js +1 -0
- package/templates/components/organisms/Contact07/Contact07.jsx +348 -0
- package/templates/components/organisms/Contact07/Contact07.propTypes.js +92 -0
- package/templates/components/organisms/Contact07/index.js +1 -0
- package/templates/components/organisms/Contact08/Contact08.jsx +178 -0
- package/templates/components/organisms/Contact08/Contact08.propTypes.js +36 -0
- package/templates/components/organisms/Contact08/index.js +1 -0
- package/templates/components/organisms/Contact09/Contact09.jsx +345 -0
- package/templates/components/organisms/Contact09/Contact09.propTypes.js +96 -0
- package/templates/components/organisms/Contact09/index.js +1 -0
- package/templates/components/organisms/Contact10/Contact10.jsx +175 -0
- package/templates/components/organisms/Contact10/Contact10.propTypes.js +55 -0
- package/templates/components/organisms/Contact10/index.js +1 -0
- package/templates/public/contact/contact01/ellipse.svg +12 -0
- package/templates/public/contact/contact01/logo-accenture.svg +6 -0
- package/templates/public/contact/contact01/logo-amart.svg +12 -0
- package/templates/public/contact/contact01/logo-brand12.png +0 -0
- package/templates/public/contact/contact01/logo-brand5.png +0 -0
- package/templates/public/contact/contact01/logo-brand7.png +0 -0
- package/templates/public/contact/contact01/logo-brand9.svg +12 -0
- package/templates/public/contact/contact01/logo-great-eastern.png +0 -0
- package/templates/public/contact/contact01/logo-kenwood.png +0 -0
- package/templates/public/contact/contact01/logo-nissan.svg +12 -0
- package/templates/public/contact/contact01/logo-suncorp.png +0 -0
- package/templates/public/contact/contact01/logo-ticketmaster.png +0 -0
- package/templates/public/contact/contact01/logo-toyota.svg +5 -0
- package/templates/public/contact/contact02/map.jpg +0 -0
- package/templates/public/contact/contact03/bg.jpg +0 -0
- package/templates/public/contact/contact03/card-photo.jpg +0 -0
- package/templates/public/contact/contact05/hero.jpg +0 -0
- package/templates/public/contact/contact05/pattern.png +0 -0
- package/templates/public/contact/contact06/hero.jpg +0 -0
- package/templates/public/contact/contact07/hero.jpg +0 -0
- 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;
|