@pradip1995/segment-jewelry-subscribe 0.1.2

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/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@pradip1995/segment-jewelry-subscribe",
3
+ "version": "0.1.2",
4
+ "license": "MIT",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "sideEffects": [
9
+ "src/segment.css"
10
+ ],
11
+ "files": [
12
+ "src",
13
+ "public"
14
+ ],
15
+ "exports": {
16
+ ".": "./src/index.ts",
17
+ "./manifest": "./src/manifest.ts"
18
+ },
19
+ "peerDependencies": {
20
+ "@pradip1995/commerce-core": "^4.0.0",
21
+ "@pradip1995/plugin-sdk": "^0.2.0",
22
+ "react": ">=19",
23
+ "react-dom": ">=19",
24
+ "next": ">=15"
25
+ },
26
+ "dependencies": {
27
+ "@pradip1995/segment-analytics": "^0.2.4",
28
+ "@pradip1995/segment-primitives": "^0.4.0",
29
+ "@pradip1995/segment-tokens": "^0.3.7"
30
+ },
31
+ "devDependencies": {
32
+ "@pradip1995/plugin-sdk": "^0.2.0",
33
+ "@types/react": "^19",
34
+ "react": "19.0.3",
35
+ "typescript": "^5.7.2"
36
+ },
37
+ "scripts": {
38
+ "typecheck": "tsc --noEmit",
39
+ "lint": "tsc --noEmit"
40
+ }
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { default } from "./segment"
2
+ export { default as manifest } from "./manifest"
@@ -0,0 +1,11 @@
1
+ import type { SegmentManifest } from "@pradip1995/plugin-sdk"
2
+
3
+ const manifest: SegmentManifest = {
4
+ id: "jewelry-subscribe",
5
+ type: "segment",
6
+ version: "0.1.0",
7
+ compatibleFramework: ["^1.0.0"],
8
+ dataKey: "jewelrySubscribe",
9
+ }
10
+
11
+ export default manifest
@@ -0,0 +1,260 @@
1
+ .jewelry-subscribe {
2
+ position: relative;
3
+ width: 100%;
4
+ overflow: hidden;
5
+ background: var(--jewelry-subscribe-bg, #d8c8dc);
6
+ }
7
+
8
+ .jewelry-subscribe__background {
9
+ position: absolute;
10
+ inset: 0;
11
+ }
12
+
13
+ .jewelry-subscribe__background::after {
14
+ content: "";
15
+ position: absolute;
16
+ inset: 0;
17
+ background: var(
18
+ --jewelry-subscribe-scrim,
19
+ linear-gradient(
20
+ 180deg,
21
+ rgba(216, 200, 220, 0.35) 0%,
22
+ rgba(216, 200, 220, 0.55) 45%,
23
+ rgba(216, 200, 220, 0.4) 100%
24
+ )
25
+ );
26
+ pointer-events: none;
27
+ }
28
+
29
+ .jewelry-subscribe__background-image {
30
+ object-fit: cover;
31
+ object-position: center center;
32
+ }
33
+
34
+ .jewelry-subscribe__background-image--mobile {
35
+ display: none;
36
+ }
37
+
38
+ .jewelry-subscribe__inner {
39
+ position: relative;
40
+ z-index: 1;
41
+ max-width: var(--container-max, 1200px);
42
+ margin: 0 auto;
43
+ padding: clamp(2.5rem, 6vw, 4.5rem) 1.25rem;
44
+ box-sizing: border-box;
45
+ }
46
+
47
+ .jewelry-subscribe__content {
48
+ max-width: 34rem;
49
+ margin: 0 auto;
50
+ text-align: center;
51
+ }
52
+
53
+ .jewelry-subscribe__eyebrow {
54
+ margin: 0 0 0.75rem;
55
+ font-family: var(--font-heading, Georgia, "Times New Roman", serif);
56
+ font-size: clamp(1.125rem, 2.2vw, 1.5rem);
57
+ font-weight: 400;
58
+ line-height: 1.2;
59
+ color: var(--jewelry-subscribe-eyebrow, #1a1a1a);
60
+ }
61
+
62
+ .jewelry-subscribe__title {
63
+ margin: 0 0 1rem;
64
+ font-family: var(--font-heading, Georgia, "Times New Roman", serif);
65
+ font-size: clamp(1.75rem, 4.5vw, 3.25rem);
66
+ font-weight: 400;
67
+ line-height: 1.12;
68
+ color: var(--jewelry-subscribe-title, #111111);
69
+ }
70
+
71
+ .jewelry-subscribe__description {
72
+ margin: 0 0 1.75rem;
73
+ font-family: var(--font-body, system-ui, sans-serif);
74
+ font-size: clamp(0.875rem, 1.5vw, 1rem);
75
+ line-height: 1.65;
76
+ color: var(--jewelry-subscribe-description, #3a3a3a);
77
+ }
78
+
79
+ .jewelry-subscribe__form {
80
+ display: flex;
81
+ align-items: stretch;
82
+ justify-content: center;
83
+ gap: 0;
84
+ width: 100%;
85
+ max-width: 32rem;
86
+ margin: 0 auto;
87
+ }
88
+
89
+ .jewelry-subscribe__field {
90
+ flex: 1;
91
+ min-width: 0;
92
+ }
93
+
94
+ .jewelry-subscribe__input {
95
+ width: 100%;
96
+ height: 100%;
97
+ min-height: 3rem;
98
+ padding: 0.875rem 1rem;
99
+ border: 1px solid rgba(26, 26, 26, 0.35);
100
+ border-right: 0;
101
+ background: rgba(255, 255, 255, 0.72);
102
+ color: var(--jewelry-subscribe-input-text, #1a1a1a);
103
+ font-family: var(--font-body, system-ui, sans-serif);
104
+ font-size: 0.875rem;
105
+ line-height: 1.4;
106
+ box-sizing: border-box;
107
+ }
108
+
109
+ .jewelry-subscribe__input::placeholder {
110
+ color: rgba(26, 26, 26, 0.55);
111
+ }
112
+
113
+ .jewelry-subscribe__input:focus {
114
+ outline: 2px solid rgba(26, 42, 68, 0.35);
115
+ outline-offset: -2px;
116
+ }
117
+
118
+ .jewelry-subscribe__submit {
119
+ flex-shrink: 0;
120
+ min-width: 7.5rem;
121
+ padding: 0.875rem 1.5rem;
122
+ border: 0;
123
+ background: var(--jewelry-subscribe-cta-bg, #1a2744);
124
+ color: #ffffff;
125
+ font-family: var(--font-body, system-ui, sans-serif);
126
+ font-size: 0.75rem;
127
+ font-weight: 600;
128
+ letter-spacing: 0.08em;
129
+ text-transform: uppercase;
130
+ cursor: pointer;
131
+ transition: background-color 0.2s ease;
132
+ }
133
+
134
+ .jewelry-subscribe__submit:hover,
135
+ .jewelry-subscribe__submit:focus-visible {
136
+ background: var(--jewelry-subscribe-cta-bg-hover, #121b30);
137
+ }
138
+
139
+ .jewelry-subscribe__submit:disabled {
140
+ opacity: 0.72;
141
+ cursor: wait;
142
+ }
143
+
144
+ .jewelry-subscribe__privacy {
145
+ display: inline-flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ gap: 0.375rem;
149
+ margin: 1rem 0 0;
150
+ max-width: 100%;
151
+ padding: 0 0.25rem;
152
+ font-family: var(--font-body, system-ui, sans-serif);
153
+ font-size: 0.75rem;
154
+ line-height: 1.4;
155
+ color: var(--jewelry-subscribe-privacy, #4a4a4a);
156
+ }
157
+
158
+ .jewelry-subscribe__privacy-icon {
159
+ flex-shrink: 0;
160
+ font-size: 0.8125rem;
161
+ line-height: 1;
162
+ }
163
+
164
+ .jewelry-subscribe__status {
165
+ margin: 0.875rem 0 0;
166
+ font-size: 0.8125rem;
167
+ line-height: 1.5;
168
+ }
169
+
170
+ .jewelry-subscribe__status--success {
171
+ color: #1f6b3f;
172
+ }
173
+
174
+ .jewelry-subscribe__status--error {
175
+ color: #9b2c2c;
176
+ }
177
+
178
+ .sr-only {
179
+ position: absolute;
180
+ width: 1px;
181
+ height: 1px;
182
+ padding: 0;
183
+ margin: -1px;
184
+ overflow: hidden;
185
+ clip: rect(0, 0, 0, 0);
186
+ white-space: nowrap;
187
+ border: 0;
188
+ }
189
+
190
+ @media (max-width: 640px) {
191
+ .jewelry-subscribe__background-image--desktop {
192
+ display: none;
193
+ }
194
+
195
+ .jewelry-subscribe__background-image--mobile {
196
+ display: block;
197
+ object-position: center center;
198
+ }
199
+
200
+ .jewelry-subscribe__background-image--single {
201
+ display: block;
202
+ object-position: center center;
203
+ }
204
+
205
+ .jewelry-subscribe__inner {
206
+ padding: 2.25rem 1rem 2.5rem;
207
+ }
208
+
209
+ .jewelry-subscribe__content {
210
+ max-width: 100%;
211
+ }
212
+
213
+ .jewelry-subscribe__eyebrow {
214
+ font-size: 1rem;
215
+ margin-bottom: 0.5rem;
216
+ }
217
+
218
+ .jewelry-subscribe__title {
219
+ font-size: clamp(1.625rem, 8vw, 2.125rem);
220
+ margin-bottom: 0.75rem;
221
+ }
222
+
223
+ .jewelry-subscribe__description {
224
+ margin-bottom: 1.25rem;
225
+ font-size: 0.875rem;
226
+ }
227
+
228
+ .jewelry-subscribe__form {
229
+ flex-direction: column;
230
+ gap: 0.75rem;
231
+ max-width: 100%;
232
+ }
233
+
234
+ .jewelry-subscribe__input {
235
+ border-right: 1px solid rgba(26, 26, 26, 0.35);
236
+ min-height: 3.125rem;
237
+ }
238
+
239
+ .jewelry-subscribe__submit {
240
+ width: 100%;
241
+ min-width: 0;
242
+ min-height: 3.125rem;
243
+ }
244
+
245
+ .jewelry-subscribe__privacy {
246
+ flex-wrap: wrap;
247
+ font-size: 0.6875rem;
248
+ }
249
+ }
250
+
251
+ @media (min-width: 768px) {
252
+ .jewelry-subscribe__inner {
253
+ padding-left: 2rem;
254
+ padding-right: 2rem;
255
+ }
256
+
257
+ .jewelry-subscribe__background::after {
258
+ background: transparent;
259
+ }
260
+ }
@@ -0,0 +1,150 @@
1
+ "use client"
2
+
3
+ import "./segment.css"
4
+ import Image from "next/image"
5
+ import { FormEvent, useState } from "react"
6
+ import { trackFormSubmit } from "@pradip1995/segment-analytics/form-events"
7
+ import { subscribeNewsletter } from "./subscribe-newsletter"
8
+
9
+ /**
10
+ * Newsletter banner. Background images must be decorative jewelry/texture only —
11
+ * do not bake title/form into the image (that doubles content on mobile).
12
+ * When `mobileBackgroundImageUrl` is omitted, the desktop image is reused.
13
+ */
14
+ export default function JewelrySubscribe({
15
+ eyebrow = "Join Our Journey",
16
+ title = "Be the First to Know",
17
+ description = "Exclusive Launch, early access & Styling Inspiration for Our Indus Crown.",
18
+ emailPlaceholder = "Enter your email...",
19
+ submitLabel = "Sign Up",
20
+ privacyText = "We Respect your Privacy, Unsubscribe at any time",
21
+ backgroundImageUrl = "/images/subscribe/subscribe-bg-desktop.png",
22
+ mobileBackgroundImageUrl,
23
+ }: {
24
+ eyebrow?: string
25
+ title?: string
26
+ description?: string
27
+ emailPlaceholder?: string
28
+ submitLabel?: string
29
+ privacyText?: string
30
+ backgroundImageUrl?: string
31
+ /** Optional. Jewelry/texture only — no baked-in copy or form. Falls back to desktop. */
32
+ mobileBackgroundImageUrl?: string
33
+ }) {
34
+ const [email, setEmail] = useState("")
35
+ const [submitStatus, setSubmitStatus] = useState<"idle" | "loading" | "success" | "error">("idle")
36
+ const [submitMessage, setSubmitMessage] = useState("")
37
+
38
+ const desktopBg = backgroundImageUrl?.trim() || mobileBackgroundImageUrl?.trim() || ""
39
+ const mobileBg = mobileBackgroundImageUrl?.trim() || desktopBg
40
+ const hasSeparateMobileBg = Boolean(mobileBg && mobileBg !== desktopBg)
41
+
42
+ const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
43
+ event.preventDefault()
44
+ if (submitStatus === "loading") return
45
+
46
+ const sanitizedEmail = email.trim()
47
+ if (!sanitizedEmail) {
48
+ setSubmitStatus("error")
49
+ setSubmitMessage("Please enter your email address.")
50
+ return
51
+ }
52
+
53
+ setSubmitStatus("loading")
54
+ setSubmitMessage("")
55
+
56
+ const result = await subscribeNewsletter({ email: sanitizedEmail })
57
+
58
+ if (result.success) {
59
+ trackFormSubmit({ form_type: "jewelry_subscribe" })
60
+ setSubmitStatus("success")
61
+ setSubmitMessage("You're subscribed. Watch your inbox for exclusive updates.")
62
+ setEmail("")
63
+ } else {
64
+ setSubmitStatus("error")
65
+ setSubmitMessage(result.error || "Something went wrong. Please try again.")
66
+ }
67
+ }
68
+
69
+ if (!desktopBg) return null
70
+
71
+ return (
72
+ <section className="jewelry-subscribe" aria-labelledby="jewelry-subscribe-title">
73
+ <div className="jewelry-subscribe__background" aria-hidden="true">
74
+ <Image
75
+ src={desktopBg}
76
+ alt=""
77
+ fill
78
+ className={
79
+ hasSeparateMobileBg
80
+ ? "jewelry-subscribe__background-image jewelry-subscribe__background-image--desktop"
81
+ : "jewelry-subscribe__background-image jewelry-subscribe__background-image--single"
82
+ }
83
+ sizes="100vw"
84
+ unoptimized
85
+ />
86
+ {hasSeparateMobileBg ? (
87
+ <Image
88
+ src={mobileBg}
89
+ alt=""
90
+ fill
91
+ className="jewelry-subscribe__background-image jewelry-subscribe__background-image--mobile"
92
+ sizes="100vw"
93
+ unoptimized
94
+ />
95
+ ) : null}
96
+ </div>
97
+
98
+ <div className="jewelry-subscribe__inner">
99
+ <div className="jewelry-subscribe__content">
100
+ {eyebrow ? <p className="jewelry-subscribe__eyebrow">{eyebrow}</p> : null}
101
+ <h2 id="jewelry-subscribe-title" className="jewelry-subscribe__title">
102
+ {title}
103
+ </h2>
104
+ {description ? <p className="jewelry-subscribe__description">{description}</p> : null}
105
+
106
+ <form onSubmit={handleSubmit} className="jewelry-subscribe__form">
107
+ <label className="jewelry-subscribe__field">
108
+ <span className="sr-only">Email</span>
109
+ <input
110
+ type="email"
111
+ value={email}
112
+ onChange={(event) => setEmail(event.target.value)}
113
+ placeholder={emailPlaceholder}
114
+ className="jewelry-subscribe__input"
115
+ required
116
+ autoComplete="email"
117
+ />
118
+ </label>
119
+ <button
120
+ type="submit"
121
+ disabled={submitStatus === "loading"}
122
+ className="jewelry-subscribe__submit"
123
+ >
124
+ {submitStatus === "loading" ? "Signing up..." : submitLabel}
125
+ </button>
126
+ </form>
127
+
128
+ {privacyText ? (
129
+ <p className="jewelry-subscribe__privacy">
130
+ <span className="jewelry-subscribe__privacy-icon" aria-hidden="true">
131
+ 🔒
132
+ </span>
133
+ {privacyText}
134
+ </p>
135
+ ) : null}
136
+
137
+ {submitStatus !== "idle" ? (
138
+ <p
139
+ role="status"
140
+ aria-live="polite"
141
+ className={`jewelry-subscribe__status jewelry-subscribe__status--${submitStatus}`}
142
+ >
143
+ {submitMessage}
144
+ </p>
145
+ ) : null}
146
+ </div>
147
+ </div>
148
+ </section>
149
+ )
150
+ }
@@ -0,0 +1,50 @@
1
+ import { loaderBus } from "@pradip1995/commerce-core/util/loader-bus"
2
+
3
+ export async function subscribeNewsletter({
4
+ email,
5
+ name,
6
+ source = "jewelry-subscribe",
7
+ }: {
8
+ email: string
9
+ name?: string
10
+ source?: string
11
+ }): Promise<{ success: boolean; error?: string }> {
12
+ const publishableKey = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
13
+ const baseUrl =
14
+ process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL || process.env.MEDUSA_BACKEND_URL || "http://localhost:9000"
15
+
16
+ if (!publishableKey) {
17
+ return { success: false, error: "Configuration error: Publishable API key is missing." }
18
+ }
19
+
20
+ try {
21
+ loaderBus.begin()
22
+ const response = await fetch(`${baseUrl}/store/contact-email-subscriptions`, {
23
+ method: "POST",
24
+ headers: {
25
+ "Content-Type": "application/json",
26
+ "x-publishable-api-key": publishableKey,
27
+ },
28
+ body: JSON.stringify({
29
+ email,
30
+ status: "subscribed",
31
+ source,
32
+ ...(name ? { name } : {}),
33
+ }),
34
+ })
35
+
36
+ if (!response.ok) {
37
+ const error = await response.text().catch(() => "Unknown error")
38
+ return { success: false, error: error || `HTTP ${response.status}: Failed to subscribe` }
39
+ }
40
+
41
+ return { success: true }
42
+ } catch (error) {
43
+ return {
44
+ success: false,
45
+ error: error instanceof Error ? error.message : "An error occurred while subscribing",
46
+ }
47
+ } finally {
48
+ loaderBus.end()
49
+ }
50
+ }