@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,223 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import SafeImage from "../../atoms/SafeImage";
|
|
5
|
+
import {
|
|
6
|
+
contact06DefaultProps,
|
|
7
|
+
contact06PropTypes,
|
|
8
|
+
} from "./Contact06.propTypes";
|
|
9
|
+
|
|
10
|
+
const underlineInputClass =
|
|
11
|
+
"w-full bg-transparent text-base text-[#2c2c2c] outline-none transition-colors duration-200 ease-out placeholder:text-transparent focus:placeholder:text-[#2c2c2c]/40";
|
|
12
|
+
|
|
13
|
+
function ChevronDownIcon({ className = "" }) {
|
|
14
|
+
return (
|
|
15
|
+
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className={className}>
|
|
16
|
+
<path
|
|
17
|
+
d="M5 8l5 5 5-5"
|
|
18
|
+
stroke="currentColor"
|
|
19
|
+
strokeWidth="1.5"
|
|
20
|
+
strokeLinecap="round"
|
|
21
|
+
strokeLinejoin="round"
|
|
22
|
+
/>
|
|
23
|
+
</svg>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ArrowUpRightIcon({ className = "" }) {
|
|
28
|
+
return (
|
|
29
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
|
|
30
|
+
<path
|
|
31
|
+
d="M7 17L17 7M17 7H9M17 7V15"
|
|
32
|
+
stroke="currentColor"
|
|
33
|
+
strokeWidth="1.5"
|
|
34
|
+
strokeLinecap="round"
|
|
35
|
+
strokeLinejoin="round"
|
|
36
|
+
/>
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function UnderlineField({ label, required = false, children }) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex h-[60px] flex-col justify-center border-b border-[rgba(44,44,44,0.1)] py-5">
|
|
44
|
+
<label className="text-xs font-medium uppercase tracking-[0.96px] text-[#2c2c2c]/60">
|
|
45
|
+
{label}
|
|
46
|
+
{required ? "*" : ""}
|
|
47
|
+
</label>
|
|
48
|
+
{children}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Contact06 — Split hero image + underline form panel.
|
|
55
|
+
* Figma node 26:15636 at 1920px.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} props - See Contact06.propTypes.js.
|
|
58
|
+
*/
|
|
59
|
+
export function Contact06({
|
|
60
|
+
headline = contact06DefaultProps.headline,
|
|
61
|
+
description = contact06DefaultProps.description,
|
|
62
|
+
heroImage = contact06DefaultProps.heroImage,
|
|
63
|
+
heroAlt = contact06DefaultProps.heroAlt,
|
|
64
|
+
subjectOptions = contact06DefaultProps.subjectOptions,
|
|
65
|
+
submitText = contact06DefaultProps.submitText,
|
|
66
|
+
formData: controlledForm,
|
|
67
|
+
onFieldChange,
|
|
68
|
+
onSubmit,
|
|
69
|
+
className = "",
|
|
70
|
+
}) {
|
|
71
|
+
const [internalForm, setInternalForm] = useState({
|
|
72
|
+
firstName: "",
|
|
73
|
+
lastName: "",
|
|
74
|
+
email: "",
|
|
75
|
+
phone: "",
|
|
76
|
+
subject: "",
|
|
77
|
+
message: "",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const form = controlledForm ?? internalForm;
|
|
81
|
+
|
|
82
|
+
const setField = (field, value) => {
|
|
83
|
+
onFieldChange?.(field, value);
|
|
84
|
+
if (!controlledForm) {
|
|
85
|
+
setInternalForm((prev) => ({ ...prev, [field]: value }));
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const handleSubmit = (event) => {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
onSubmit?.(form);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<section
|
|
96
|
+
className={["relative w-full overflow-hidden bg-[#fafafa]", className]
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
.join(" ")}
|
|
99
|
+
data-contact="contact06"
|
|
100
|
+
>
|
|
101
|
+
<div className="relative mx-auto flex w-full max-w-[1920px] flex-col lg:min-h-[700px] lg:flex-row xl:min-h-[1043px]">
|
|
102
|
+
{/* Left — hero image */}
|
|
103
|
+
<div className="relative h-[50vh] w-full shrink-0 lg:h-auto lg:min-h-[700px] lg:w-1/2 xl:min-h-[1043px]">
|
|
104
|
+
<SafeImage
|
|
105
|
+
src={heroImage}
|
|
106
|
+
alt={heroAlt}
|
|
107
|
+
fill
|
|
108
|
+
className="object-cover object-center"
|
|
109
|
+
sizes="(max-width: 1024px) 100vw, 50vw"
|
|
110
|
+
priority
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Right — form panel */}
|
|
115
|
+
<div className="flex w-full items-center bg-[#fafafa] px-4 py-12 sm:px-6 md:px-10 lg:w-1/2 lg:px-16 lg:py-16 xl:px-[80px] xl:py-20">
|
|
116
|
+
<form
|
|
117
|
+
onSubmit={handleSubmit}
|
|
118
|
+
className="flex w-full max-w-[608px] flex-col gap-10 lg:gap-[60px]"
|
|
119
|
+
>
|
|
120
|
+
<div className="flex flex-col gap-5 text-[#2c2c2c]">
|
|
121
|
+
<h2 className="font-serif text-3xl leading-[1.1] tracking-[-0.96px] sm:text-4xl xl:text-[48px]">
|
|
122
|
+
{headline}
|
|
123
|
+
</h2>
|
|
124
|
+
<p className="font-display text-base leading-[1.4] tracking-[-0.36px] opacity-80 sm:text-lg">
|
|
125
|
+
{description}
|
|
126
|
+
</p>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div className="flex flex-col gap-5">
|
|
130
|
+
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
|
131
|
+
<UnderlineField label="First name" required>
|
|
132
|
+
<input
|
|
133
|
+
type="text"
|
|
134
|
+
value={form.firstName}
|
|
135
|
+
onChange={(e) => setField("firstName", e.target.value)}
|
|
136
|
+
className={underlineInputClass}
|
|
137
|
+
required
|
|
138
|
+
/>
|
|
139
|
+
</UnderlineField>
|
|
140
|
+
<UnderlineField label="Last name" required>
|
|
141
|
+
<input
|
|
142
|
+
type="text"
|
|
143
|
+
value={form.lastName}
|
|
144
|
+
onChange={(e) => setField("lastName", e.target.value)}
|
|
145
|
+
className={underlineInputClass}
|
|
146
|
+
required
|
|
147
|
+
/>
|
|
148
|
+
</UnderlineField>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
|
|
152
|
+
<UnderlineField label="Email address" required>
|
|
153
|
+
<input
|
|
154
|
+
type="email"
|
|
155
|
+
value={form.email}
|
|
156
|
+
onChange={(e) => setField("email", e.target.value)}
|
|
157
|
+
className={underlineInputClass}
|
|
158
|
+
required
|
|
159
|
+
/>
|
|
160
|
+
</UnderlineField>
|
|
161
|
+
<UnderlineField label="Phone number">
|
|
162
|
+
<input
|
|
163
|
+
type="tel"
|
|
164
|
+
value={form.phone}
|
|
165
|
+
onChange={(e) => setField("phone", e.target.value)}
|
|
166
|
+
className={underlineInputClass}
|
|
167
|
+
/>
|
|
168
|
+
</UnderlineField>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div className="relative flex h-[60px] items-center border-b border-[rgba(44,44,44,0.1)] py-5">
|
|
172
|
+
<label className="sr-only">Select subject</label>
|
|
173
|
+
<select
|
|
174
|
+
value={form.subject}
|
|
175
|
+
onChange={(e) => setField("subject", e.target.value)}
|
|
176
|
+
className={`${underlineInputClass} appearance-none pr-8 text-xs font-medium uppercase tracking-[0.96px] text-[#2c2c2c]/60`}
|
|
177
|
+
>
|
|
178
|
+
<option value="" disabled>
|
|
179
|
+
Select subject
|
|
180
|
+
</option>
|
|
181
|
+
{subjectOptions.map((option) => (
|
|
182
|
+
<option key={option} value={option} className="normal-case">
|
|
183
|
+
{option}
|
|
184
|
+
</option>
|
|
185
|
+
))}
|
|
186
|
+
</select>
|
|
187
|
+
<ChevronDownIcon className="pointer-events-none absolute right-0 size-5 text-[#2c2c2c]" />
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div className="flex min-h-[111px] flex-col border-b border-[rgba(44,44,44,0.1)] py-5">
|
|
191
|
+
<label className="text-xs font-medium uppercase tracking-[0.96px] text-[#2c2c2c]/60">
|
|
192
|
+
Write your message here
|
|
193
|
+
</label>
|
|
194
|
+
<textarea
|
|
195
|
+
value={form.message}
|
|
196
|
+
onChange={(e) => setField("message", e.target.value)}
|
|
197
|
+
rows={3}
|
|
198
|
+
className={`${underlineInputClass} mt-1 min-h-[60px] resize-none`}
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<button
|
|
204
|
+
type="submit"
|
|
205
|
+
className="flex h-12 w-max max-w-full items-stretch overflow-hidden transition-opacity duration-200 ease-out hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
206
|
+
>
|
|
207
|
+
<span className="flex items-center justify-center bg-[#146d38] px-6 py-3 font-display text-sm font-semibold uppercase tracking-[1.12px] text-white">
|
|
208
|
+
{submitText}
|
|
209
|
+
</span>
|
|
210
|
+
<span className="flex size-12 shrink-0 items-center justify-center bg-[#146d38] text-white">
|
|
211
|
+
<ArrowUpRightIcon className="size-6" />
|
|
212
|
+
</span>
|
|
213
|
+
</button>
|
|
214
|
+
</form>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</section>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Contact06.propTypes = contact06PropTypes;
|
|
222
|
+
|
|
223
|
+
export default Contact06;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
|
|
3
|
+
export const contact06PropTypes = {
|
|
4
|
+
headline: PropTypes.string,
|
|
5
|
+
description: PropTypes.string,
|
|
6
|
+
heroImage: PropTypes.string,
|
|
7
|
+
heroAlt: PropTypes.string,
|
|
8
|
+
subjectOptions: PropTypes.arrayOf(PropTypes.string),
|
|
9
|
+
submitText: PropTypes.string,
|
|
10
|
+
formData: PropTypes.shape({
|
|
11
|
+
firstName: PropTypes.string,
|
|
12
|
+
lastName: PropTypes.string,
|
|
13
|
+
email: PropTypes.string,
|
|
14
|
+
phone: PropTypes.string,
|
|
15
|
+
subject: PropTypes.string,
|
|
16
|
+
message: PropTypes.string,
|
|
17
|
+
}),
|
|
18
|
+
onFieldChange: PropTypes.func,
|
|
19
|
+
onSubmit: PropTypes.func,
|
|
20
|
+
className: PropTypes.string,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const contact06DefaultProps = {
|
|
24
|
+
headline: "Prefer to reach out online",
|
|
25
|
+
description:
|
|
26
|
+
"Submit your message and we'll connect you with the right specialist to support your project.",
|
|
27
|
+
heroImage: "/contact/contact06/hero.jpg",
|
|
28
|
+
heroAlt: "Luxury spa bathroom with tropical view",
|
|
29
|
+
subjectOptions: [
|
|
30
|
+
"General inquiry",
|
|
31
|
+
"Project consultation",
|
|
32
|
+
"Partnership",
|
|
33
|
+
"Support",
|
|
34
|
+
],
|
|
35
|
+
submitText: "submit your inquires",
|
|
36
|
+
formData: undefined,
|
|
37
|
+
onFieldChange: undefined,
|
|
38
|
+
onSubmit: undefined,
|
|
39
|
+
className: "",
|
|
40
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Contact06, default } from "./Contact06";
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import SafeImage from "../../atoms/SafeImage";
|
|
5
|
+
import {
|
|
6
|
+
contact07DefaultProps,
|
|
7
|
+
contact07PropTypes,
|
|
8
|
+
} from "./Contact07.propTypes";
|
|
9
|
+
|
|
10
|
+
const fieldClassName =
|
|
11
|
+
"h-[52px] w-full bg-white px-5 text-sm tracking-[0.14px] text-[#1b1e2e] outline-none transition-colors duration-200 ease-out placeholder:text-[#7e8088] focus:ring-1 focus:ring-[#020617]/20";
|
|
12
|
+
|
|
13
|
+
function ChevronDownIcon({ className = "" }) {
|
|
14
|
+
return (
|
|
15
|
+
<svg viewBox="0 0 18 18" fill="none" aria-hidden="true" className={className}>
|
|
16
|
+
<path
|
|
17
|
+
d="M4.5 7l4.5 4.5L13.5 7"
|
|
18
|
+
stroke="currentColor"
|
|
19
|
+
strokeWidth="1.5"
|
|
20
|
+
strokeLinecap="round"
|
|
21
|
+
strokeLinejoin="round"
|
|
22
|
+
/>
|
|
23
|
+
</svg>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ChevronUpIcon({ className = "" }) {
|
|
28
|
+
return (
|
|
29
|
+
<svg viewBox="0 0 18 18" fill="none" aria-hidden="true" className={className}>
|
|
30
|
+
<path
|
|
31
|
+
d="M4.5 11l4.5-4.5L13.5 11"
|
|
32
|
+
stroke="currentColor"
|
|
33
|
+
strokeWidth="1.5"
|
|
34
|
+
strokeLinecap="round"
|
|
35
|
+
strokeLinejoin="round"
|
|
36
|
+
/>
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function MailIcon({ className = "" }) {
|
|
42
|
+
return (
|
|
43
|
+
<svg viewBox="0 0 14 14" fill="none" aria-hidden="true" className={className}>
|
|
44
|
+
<rect x="1" y="2.5" width="12" height="9" rx="1" stroke="currentColor" strokeWidth="1" />
|
|
45
|
+
<path d="M1 4l6 4 6-4" stroke="currentColor" strokeWidth="1" strokeLinecap="round" />
|
|
46
|
+
</svg>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function PhoneIcon({ className = "" }) {
|
|
51
|
+
return (
|
|
52
|
+
<svg viewBox="0 0 14 14" fill="none" aria-hidden="true" className={className}>
|
|
53
|
+
<path
|
|
54
|
+
d="M3.5 1.5h2l1 3-1.5 1a6 6 0 003 3L9 7l3 1v2a1 1 0 01-1 1A9.5 9.5 0 011.5 3.5a1 1 0 011-2z"
|
|
55
|
+
stroke="currentColor"
|
|
56
|
+
strokeWidth="1"
|
|
57
|
+
strokeLinecap="round"
|
|
58
|
+
strokeLinejoin="round"
|
|
59
|
+
/>
|
|
60
|
+
</svg>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ClockIcon({ className = "" }) {
|
|
65
|
+
return (
|
|
66
|
+
<svg viewBox="0 0 14 14" fill="none" aria-hidden="true" className={className}>
|
|
67
|
+
<circle cx="7" cy="7" r="5.5" stroke="currentColor" strokeWidth="1" />
|
|
68
|
+
<path d="M7 4v3l2 1.5" stroke="currentColor" strokeWidth="1" strokeLinecap="round" />
|
|
69
|
+
</svg>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function OfficeColumn({ office }) {
|
|
74
|
+
return (
|
|
75
|
+
<div className="flex min-w-0 flex-1 flex-col gap-3">
|
|
76
|
+
<p className="text-xs font-medium uppercase tracking-[0.96px] text-[#020617]">
|
|
77
|
+
{office.name}
|
|
78
|
+
</p>
|
|
79
|
+
<div className="h-px w-full bg-[#e1e2e3]" />
|
|
80
|
+
<div className="flex flex-col gap-2.5 text-sm leading-[1.2] tracking-[0.14px] text-[#343744]">
|
|
81
|
+
{office.email ? (
|
|
82
|
+
<div className="flex items-center gap-2">
|
|
83
|
+
<MailIcon className="size-3.5 shrink-0 text-[#343744]" />
|
|
84
|
+
<a
|
|
85
|
+
href={`mailto:${office.email}`}
|
|
86
|
+
className="transition-opacity duration-200 hover:opacity-80"
|
|
87
|
+
>
|
|
88
|
+
{office.email}
|
|
89
|
+
</a>
|
|
90
|
+
</div>
|
|
91
|
+
) : null}
|
|
92
|
+
{office.phone ? (
|
|
93
|
+
<div className="flex items-center gap-2">
|
|
94
|
+
<PhoneIcon className="size-3.5 shrink-0 text-[#343744]" />
|
|
95
|
+
<a
|
|
96
|
+
href={`tel:${office.phone.replace(/\s/g, "")}`}
|
|
97
|
+
className="transition-opacity duration-200 hover:opacity-80"
|
|
98
|
+
>
|
|
99
|
+
{office.phone}
|
|
100
|
+
</a>
|
|
101
|
+
</div>
|
|
102
|
+
) : null}
|
|
103
|
+
{office.hours ? (
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
<ClockIcon className="size-3.5 shrink-0 text-[#343744]" />
|
|
106
|
+
<span>{office.hours}</span>
|
|
107
|
+
</div>
|
|
108
|
+
) : null}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Contact07 — Gentle Park split contact with overlaid form and FAQ.
|
|
116
|
+
* Figma node 26:15698 at 1920px (navbar excluded).
|
|
117
|
+
*
|
|
118
|
+
* @param {object} props - See Contact07.propTypes.js.
|
|
119
|
+
*/
|
|
120
|
+
export function Contact07({
|
|
121
|
+
headline = contact07DefaultProps.headline,
|
|
122
|
+
description = contact07DefaultProps.description,
|
|
123
|
+
heroImage = contact07DefaultProps.heroImage,
|
|
124
|
+
heroAlt = contact07DefaultProps.heroAlt,
|
|
125
|
+
offices = contact07DefaultProps.offices,
|
|
126
|
+
topicOptions = contact07DefaultProps.topicOptions,
|
|
127
|
+
submitText = contact07DefaultProps.submitText,
|
|
128
|
+
faqTitle = contact07DefaultProps.faqTitle,
|
|
129
|
+
faqItems = contact07DefaultProps.faqItems,
|
|
130
|
+
defaultOpenFaq = contact07DefaultProps.defaultOpenFaq,
|
|
131
|
+
activeFaq: controlledFaq,
|
|
132
|
+
onFaqChange,
|
|
133
|
+
formData: controlledForm,
|
|
134
|
+
onFieldChange,
|
|
135
|
+
onSubmit,
|
|
136
|
+
showFaq = contact07DefaultProps.showFaq,
|
|
137
|
+
className = "",
|
|
138
|
+
}) {
|
|
139
|
+
const [internalFaq, setInternalFaq] = useState(defaultOpenFaq);
|
|
140
|
+
const [internalForm, setInternalForm] = useState({
|
|
141
|
+
fullName: "",
|
|
142
|
+
email: "",
|
|
143
|
+
phone: "",
|
|
144
|
+
orderId: "",
|
|
145
|
+
topic: "",
|
|
146
|
+
message: "",
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const activeFaq = controlledFaq ?? internalFaq;
|
|
150
|
+
const form = controlledForm ?? internalForm;
|
|
151
|
+
|
|
152
|
+
const setField = (field, value) => {
|
|
153
|
+
onFieldChange?.(field, value);
|
|
154
|
+
if (!controlledForm) {
|
|
155
|
+
setInternalForm((prev) => ({ ...prev, [field]: value }));
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const toggleFaq = (index) => {
|
|
160
|
+
const next = activeFaq === index ? -1 : index;
|
|
161
|
+
onFaqChange?.(next);
|
|
162
|
+
if (controlledFaq === undefined) setInternalFaq(next);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const handleSubmit = (event) => {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
onSubmit?.(form);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<section
|
|
172
|
+
className={["relative w-full overflow-hidden bg-[#fafafa]", className]
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
.join(" ")}
|
|
175
|
+
data-contact="contact07"
|
|
176
|
+
>
|
|
177
|
+
{/* Hero + form */}
|
|
178
|
+
<div className="relative mx-auto w-full max-w-[1920px]">
|
|
179
|
+
<div className="relative flex flex-col lg:min-h-[800px] lg:flex-row xl:min-h-[1032px]">
|
|
180
|
+
{/* Left — copy + offices */}
|
|
181
|
+
<div className="relative z-10 flex w-full flex-col justify-center gap-8 bg-[#fafafa] px-4 py-12 sm:px-6 md:px-10 lg:w-1/2 lg:gap-[34px] lg:px-10 lg:py-16 xl:px-[160px] xl:py-20">
|
|
182
|
+
<div className="flex max-w-[633px] flex-col gap-[34px]">
|
|
183
|
+
<h2 className="text-4xl font-light capitalize leading-none tracking-[-1px] text-[#020617] sm:text-5xl xl:text-[50px]">
|
|
184
|
+
{headline}
|
|
185
|
+
</h2>
|
|
186
|
+
<p className="text-base leading-[1.1] tracking-[0.16px] text-[#343744]">
|
|
187
|
+
{description}
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div className="flex max-w-[633px] flex-col gap-[34px]">
|
|
192
|
+
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 sm:gap-[34px]">
|
|
193
|
+
{offices.map((office) => (
|
|
194
|
+
<OfficeColumn key={office.name} office={office} />
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Right — hero image + overlaid form */}
|
|
201
|
+
<div className="relative w-full lg:w-1/2 lg:min-h-[800px] xl:min-h-[1032px]">
|
|
202
|
+
<div className="relative h-[50vh] w-full lg:absolute lg:inset-0 lg:h-full">
|
|
203
|
+
<SafeImage
|
|
204
|
+
src={heroImage}
|
|
205
|
+
alt={heroAlt}
|
|
206
|
+
fill
|
|
207
|
+
className="object-cover object-center"
|
|
208
|
+
sizes="(max-width: 1024px) 100vw, 50vw"
|
|
209
|
+
priority
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Form overlay — flows below image on mobile, absolute on desktop */}
|
|
214
|
+
<div className="relative z-10 px-4 py-8 sm:px-6 md:px-10 lg:absolute lg:inset-0 lg:flex lg:items-center lg:justify-end lg:px-10 lg:py-0 xl:pr-[68px]">
|
|
215
|
+
<form
|
|
216
|
+
onSubmit={handleSubmit}
|
|
217
|
+
className="flex w-full max-w-[500px] flex-col gap-2.5 bg-[#fafafa]/95 p-4 backdrop-blur-sm lg:bg-transparent lg:p-0 lg:backdrop-blur-none"
|
|
218
|
+
>
|
|
219
|
+
<input
|
|
220
|
+
type="text"
|
|
221
|
+
value={form.fullName}
|
|
222
|
+
onChange={(e) => setField("fullName", e.target.value)}
|
|
223
|
+
placeholder="Full Name*"
|
|
224
|
+
className={fieldClassName}
|
|
225
|
+
required
|
|
226
|
+
/>
|
|
227
|
+
<input
|
|
228
|
+
type="email"
|
|
229
|
+
value={form.email}
|
|
230
|
+
onChange={(e) => setField("email", e.target.value)}
|
|
231
|
+
placeholder="Email Address*"
|
|
232
|
+
className={fieldClassName}
|
|
233
|
+
required
|
|
234
|
+
/>
|
|
235
|
+
<input
|
|
236
|
+
type="tel"
|
|
237
|
+
value={form.phone}
|
|
238
|
+
onChange={(e) => setField("phone", e.target.value)}
|
|
239
|
+
placeholder="Phone Number (Optional)"
|
|
240
|
+
className={fieldClassName}
|
|
241
|
+
/>
|
|
242
|
+
<input
|
|
243
|
+
type="text"
|
|
244
|
+
value={form.orderId}
|
|
245
|
+
onChange={(e) => setField("orderId", e.target.value)}
|
|
246
|
+
placeholder="Order ID (Optional)"
|
|
247
|
+
className={fieldClassName}
|
|
248
|
+
/>
|
|
249
|
+
<div className="relative">
|
|
250
|
+
<select
|
|
251
|
+
value={form.topic}
|
|
252
|
+
onChange={(e) => setField("topic", e.target.value)}
|
|
253
|
+
className={`${fieldClassName} appearance-none pr-10 text-[#7e8088]`}
|
|
254
|
+
>
|
|
255
|
+
<option value="" disabled>
|
|
256
|
+
Select topic
|
|
257
|
+
</option>
|
|
258
|
+
{topicOptions.map((option) => (
|
|
259
|
+
<option key={option} value={option} className="text-[#1b1e2e]">
|
|
260
|
+
{option}
|
|
261
|
+
</option>
|
|
262
|
+
))}
|
|
263
|
+
</select>
|
|
264
|
+
<ChevronDownIcon className="pointer-events-none absolute right-5 top-1/2 size-[18px] -translate-y-1/2 text-[#7e8088]" />
|
|
265
|
+
</div>
|
|
266
|
+
<textarea
|
|
267
|
+
value={form.message}
|
|
268
|
+
onChange={(e) => setField("message", e.target.value)}
|
|
269
|
+
placeholder="Your message"
|
|
270
|
+
rows={5}
|
|
271
|
+
className="min-h-[150px] w-full resize-none bg-white px-5 py-4 text-sm tracking-[0.14px] text-[#1b1e2e] outline-none transition-colors duration-200 ease-out placeholder:text-[#7e8088] focus:ring-1 focus:ring-[#020617]/20"
|
|
272
|
+
/>
|
|
273
|
+
<button
|
|
274
|
+
type="submit"
|
|
275
|
+
className="flex h-[52px] w-full items-center justify-center bg-[#1b1e2e] px-10 py-[17px] text-sm font-semibold uppercase tracking-[0.98px] text-white transition-colors duration-200 ease-out hover:bg-[#020617] focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
276
|
+
>
|
|
277
|
+
{submitText}
|
|
278
|
+
</button>
|
|
279
|
+
</form>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
{/* FAQ */}
|
|
286
|
+
{showFaq && faqItems?.length ? (
|
|
287
|
+
<div className="px-4 py-16 sm:px-6 md:px-10 lg:px-20 lg:py-24 xl:px-[160px]">
|
|
288
|
+
<div className="mx-auto flex w-full max-w-[955px] flex-col items-center gap-10 lg:gap-[60px]">
|
|
289
|
+
<h2 className="w-full text-center text-4xl font-light capitalize leading-none tracking-[-1px] text-[#020617] sm:text-5xl xl:text-[50px]">
|
|
290
|
+
{faqTitle}
|
|
291
|
+
</h2>
|
|
292
|
+
|
|
293
|
+
<div className="flex w-full flex-col gap-[30px]">
|
|
294
|
+
{faqItems.map((item, index) => {
|
|
295
|
+
const isOpen = activeFaq === index;
|
|
296
|
+
const isLast = index === faqItems.length - 1;
|
|
297
|
+
return (
|
|
298
|
+
<div key={item.question} className="flex flex-col gap-[30px]">
|
|
299
|
+
<div className="flex flex-col gap-[30px]">
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
onClick={() => toggleFaq(index)}
|
|
303
|
+
className="flex w-full items-center justify-between gap-4 text-left"
|
|
304
|
+
aria-expanded={isOpen}
|
|
305
|
+
>
|
|
306
|
+
<span className="text-lg font-medium leading-none text-[#020617] sm:text-2xl">
|
|
307
|
+
{item.question}
|
|
308
|
+
</span>
|
|
309
|
+
<span className="shrink-0 text-[#020617]">
|
|
310
|
+
{isOpen ? (
|
|
311
|
+
<ChevronUpIcon className="size-[18px]" />
|
|
312
|
+
) : (
|
|
313
|
+
<ChevronDownIcon className="size-[18px]" />
|
|
314
|
+
)}
|
|
315
|
+
</span>
|
|
316
|
+
</button>
|
|
317
|
+
<div
|
|
318
|
+
className={[
|
|
319
|
+
"grid motion-reduce:transition-none transition-all duration-300 ease-out",
|
|
320
|
+
isOpen && item.answer
|
|
321
|
+
? "grid-rows-[1fr] opacity-100"
|
|
322
|
+
: "grid-rows-[0fr] opacity-0",
|
|
323
|
+
].join(" ")}
|
|
324
|
+
>
|
|
325
|
+
<div className="overflow-hidden">
|
|
326
|
+
{item.answer ? (
|
|
327
|
+
<p className="text-base leading-[1.1] tracking-[0.16px] text-[#343744]">
|
|
328
|
+
{item.answer}
|
|
329
|
+
</p>
|
|
330
|
+
) : null}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
{!isLast ? <div className="h-px w-full bg-[#e1e2e3]" /> : null}
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
})}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
) : null}
|
|
342
|
+
</section>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
Contact07.propTypes = contact07PropTypes;
|
|
347
|
+
|
|
348
|
+
export default Contact07;
|