@pradip1995/segment-beauty-reviews 0.1.1

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,38 @@
1
+ {
2
+ "name": "@pradip1995/segment-beauty-reviews",
3
+ "version": "0.1.1",
4
+ "license": "MIT",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "sideEffects": [
9
+ "src/segment.css"
10
+ ],
11
+ "files": [
12
+ "src"
13
+ ],
14
+ "exports": {
15
+ ".": "./src/index.ts",
16
+ "./manifest": "./src/manifest.ts"
17
+ },
18
+ "peerDependencies": {
19
+ "@pradip1995/plugin-sdk": "^0.2.0",
20
+ "react": ">=19",
21
+ "react-dom": ">=19",
22
+ "next": ">=15"
23
+ },
24
+ "dependencies": {
25
+ "@pradip1995/segment-primitives": "0.3.0",
26
+ "@pradip1995/segment-tokens": "0.3.2"
27
+ },
28
+ "devDependencies": {
29
+ "@pradip1995/plugin-sdk": "^0.2.0",
30
+ "@types/react": "^19",
31
+ "react": "19.0.3",
32
+ "typescript": "^5.7.2"
33
+ },
34
+ "scripts": {
35
+ "typecheck": "tsc --noEmit",
36
+ "lint": "tsc --noEmit"
37
+ }
38
+ }
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: "beauty-reviews",
5
+ type: "segment",
6
+ version: "0.1.0",
7
+ compatibleFramework: ["^1.0.0"],
8
+ dataKey: "beautyReviews",
9
+ }
10
+
11
+ export default manifest
@@ -0,0 +1,283 @@
1
+ .beauty-reviews {
2
+ width: 100%;
3
+ background: linear-gradient(180deg, #faf7f5 0%, #ffffff 42%, #faf7f5 100%);
4
+ padding: 3.5rem 0 4rem;
5
+ overflow: hidden;
6
+ }
7
+
8
+ @media (min-width: 1024px) {
9
+ .beauty-reviews {
10
+ padding: 4.5rem 0 5rem;
11
+ }
12
+ }
13
+
14
+ .beauty-reviews__inner {
15
+ max-width: var(--container-max);
16
+ margin: 0 auto;
17
+ padding: 0 1rem;
18
+ }
19
+
20
+ @media (min-width: 640px) {
21
+ .beauty-reviews__inner {
22
+ padding: 0 1.5rem;
23
+ }
24
+ }
25
+
26
+ .beauty-reviews__header {
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: 1.5rem;
30
+ margin-bottom: 2rem;
31
+ }
32
+
33
+ @media (min-width: 768px) {
34
+ .beauty-reviews__header {
35
+ flex-direction: row;
36
+ align-items: flex-end;
37
+ justify-content: space-between;
38
+ gap: 2rem;
39
+ margin-bottom: 2.25rem;
40
+ }
41
+ }
42
+
43
+ .beauty-reviews__header-copy {
44
+ max-width: 38rem;
45
+ }
46
+
47
+ .beauty-reviews__eyebrow {
48
+ margin: 0 0 0.75rem;
49
+ font-size: 0.6875rem;
50
+ font-weight: 600;
51
+ letter-spacing: 0.22em;
52
+ text-transform: uppercase;
53
+ color: var(--color-brand-accent);
54
+ }
55
+
56
+ .beauty-reviews__title {
57
+ margin: 0 0 0.875rem;
58
+ font-family: var(--font-heading);
59
+ font-size: clamp(1.875rem, 3.5vw, 2.875rem);
60
+ font-weight: 500;
61
+ line-height: 1.12;
62
+ letter-spacing: 0.02em;
63
+ color: var(--color-text-heading);
64
+ }
65
+
66
+ .beauty-reviews__description {
67
+ margin: 0;
68
+ font-size: 0.9375rem;
69
+ line-height: 1.7;
70
+ color: var(--color-text-body);
71
+ }
72
+
73
+ .beauty-reviews__aggregate-card {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 1rem;
77
+ flex-shrink: 0;
78
+ padding: 1.125rem 1.25rem;
79
+ background: #ffffff;
80
+ border: 1px solid rgba(139, 58, 98, 0.14);
81
+ box-shadow: 0 12px 28px rgba(42, 16, 32, 0.06);
82
+ }
83
+
84
+ .beauty-reviews__score {
85
+ font-family: var(--font-heading);
86
+ font-size: 2.75rem;
87
+ font-weight: 500;
88
+ line-height: 1;
89
+ color: var(--color-text-heading);
90
+ }
91
+
92
+ .beauty-reviews__aggregate-meta {
93
+ display: flex;
94
+ flex-direction: column;
95
+ gap: 0.35rem;
96
+ }
97
+
98
+ .beauty-reviews__aggregate-label {
99
+ margin: 0;
100
+ font-size: 0.75rem;
101
+ letter-spacing: 0.04em;
102
+ color: var(--color-text-muted);
103
+ }
104
+
105
+ .beauty-reviews__stars {
106
+ display: flex;
107
+ gap: 0.15rem;
108
+ line-height: 1;
109
+ }
110
+
111
+ .beauty-reviews__stars--lg .beauty-reviews__star {
112
+ font-size: 1rem;
113
+ }
114
+
115
+ .beauty-reviews__star {
116
+ color: #e0d5da;
117
+ font-size: 0.8125rem;
118
+ }
119
+
120
+ .beauty-reviews__star--filled {
121
+ color: #c9a86c;
122
+ }
123
+
124
+ .beauty-reviews__carousel {
125
+ display: grid;
126
+ grid-template-columns: auto 1fr auto;
127
+ align-items: center;
128
+ gap: 0.5rem;
129
+ }
130
+
131
+ @media (min-width: 768px) {
132
+ .beauty-reviews__carousel {
133
+ gap: 0.75rem;
134
+ }
135
+ }
136
+
137
+ .beauty-reviews__nav {
138
+ flex-shrink: 0;
139
+ width: 2.5rem;
140
+ height: 2.5rem;
141
+ display: inline-flex;
142
+ align-items: center;
143
+ justify-content: center;
144
+ border: 1px solid rgba(139, 58, 98, 0.2);
145
+ background: #ffffff;
146
+ color: var(--color-text-heading);
147
+ font-size: 1.5rem;
148
+ line-height: 1;
149
+ cursor: pointer;
150
+ transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, opacity 0.2s ease;
151
+ }
152
+
153
+ .beauty-reviews__nav:hover:not(:disabled) {
154
+ background: #000000;
155
+ border-color: #000000;
156
+ color: #ffffff;
157
+ }
158
+
159
+ .beauty-reviews__nav:disabled {
160
+ opacity: 0.35;
161
+ cursor: not-allowed;
162
+ }
163
+
164
+ .beauty-reviews__track-wrap {
165
+ position: relative;
166
+ min-width: 0;
167
+ mask-image: linear-gradient(to right, transparent, black 3%, black 97%, transparent);
168
+ }
169
+
170
+ .beauty-reviews__track {
171
+ display: flex;
172
+ gap: 1rem;
173
+ overflow-x: auto;
174
+ scroll-snap-type: x mandatory;
175
+ scroll-behavior: smooth;
176
+ -webkit-overflow-scrolling: touch;
177
+ scrollbar-width: none;
178
+ padding: 0.25rem 0.5rem 1rem;
179
+ }
180
+
181
+ .beauty-reviews__track::-webkit-scrollbar {
182
+ display: none;
183
+ }
184
+
185
+ .beauty-reviews__card {
186
+ position: relative;
187
+ flex: 0 0 clamp(280px, 78vw, 340px);
188
+ scroll-snap-align: start;
189
+ display: flex;
190
+ flex-direction: column;
191
+ gap: 0.875rem;
192
+ min-height: 15.5rem;
193
+ padding: 1.625rem 1.5rem 1.375rem;
194
+ background: #ffffff;
195
+ border: 1px solid rgba(139, 58, 98, 0.12);
196
+ box-shadow: 0 14px 36px rgba(42, 16, 32, 0.07);
197
+ transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
198
+ }
199
+
200
+ .beauty-reviews__card:hover {
201
+ transform: translateY(-0.25rem);
202
+ border-color: rgba(139, 58, 98, 0.28);
203
+ box-shadow: 0 20px 44px rgba(42, 16, 32, 0.1);
204
+ }
205
+
206
+ .beauty-reviews__quote-mark {
207
+ position: absolute;
208
+ top: 0.75rem;
209
+ right: 1rem;
210
+ font-family: var(--font-heading);
211
+ font-size: 3rem;
212
+ line-height: 1;
213
+ color: rgba(139, 58, 98, 0.12);
214
+ pointer-events: none;
215
+ }
216
+
217
+ .beauty-reviews__product {
218
+ align-self: flex-start;
219
+ padding: 0.3rem 0.7rem;
220
+ font-size: 0.625rem;
221
+ font-weight: 600;
222
+ letter-spacing: 0.14em;
223
+ text-transform: uppercase;
224
+ color: var(--color-brand-accent);
225
+ background: var(--color-brand-accent-muted);
226
+ border: 1px solid rgba(139, 58, 98, 0.12);
227
+ }
228
+
229
+ .beauty-reviews__quote {
230
+ margin: 0;
231
+ flex: 1;
232
+ font-family: var(--font-quote);
233
+ font-size: 1.125rem;
234
+ line-height: 1.55;
235
+ color: var(--color-text-heading);
236
+ }
237
+
238
+ .beauty-reviews__card-footer {
239
+ display: flex;
240
+ flex-direction: column;
241
+ gap: 0.5rem;
242
+ margin-top: auto;
243
+ padding-top: 0.75rem;
244
+ border-top: 1px solid rgba(139, 58, 98, 0.1);
245
+ }
246
+
247
+ .beauty-reviews__meta {
248
+ margin: 0;
249
+ display: flex;
250
+ flex-wrap: wrap;
251
+ align-items: center;
252
+ gap: 0.375rem;
253
+ font-size: 0.8125rem;
254
+ }
255
+
256
+ .beauty-reviews__name {
257
+ font-weight: 600;
258
+ color: var(--color-text-heading);
259
+ }
260
+
261
+ .beauty-reviews__dot {
262
+ color: var(--color-brand-accent-border);
263
+ }
264
+
265
+ .beauty-reviews__verified {
266
+ color: var(--color-text-muted);
267
+ }
268
+
269
+ .beauty-reviews__scroll-hint {
270
+ margin: 0.75rem 0 0;
271
+ text-align: center;
272
+ font-size: 0.6875rem;
273
+ letter-spacing: 0.12em;
274
+ text-transform: uppercase;
275
+ color: var(--color-text-muted);
276
+ }
277
+
278
+ @media (min-width: 768px) {
279
+ .beauty-reviews__scroll-hint {
280
+ text-align: right;
281
+ padding-right: 3.25rem;
282
+ }
283
+ }
@@ -0,0 +1,211 @@
1
+ "use client"
2
+
3
+ import "./segment.css"
4
+ import { useCallback, useEffect, useRef, useState } from "react"
5
+
6
+ type ReviewItem = {
7
+ id: string
8
+ text: string
9
+ name: string
10
+ rating: number
11
+ product?: string
12
+ }
13
+
14
+ const DEFAULT_REVIEWS: ReviewItem[] = [
15
+ {
16
+ id: "1",
17
+ name: "Priya S.",
18
+ rating: 5,
19
+ product: "Matte Lipstick",
20
+ text: "The lipstick shade is gorgeous and stays all day without drying my lips.",
21
+ },
22
+ {
23
+ id: "2",
24
+ name: "Ananya K.",
25
+ rating: 5,
26
+ product: "Vitamin C Serum",
27
+ text: "My skin feels brighter after two weeks. Lightweight, absorbs fast, no sticky feel.",
28
+ },
29
+ {
30
+ id: "3",
31
+ name: "Meera R.",
32
+ rating: 5,
33
+ product: "Foundation",
34
+ text: "Fast delivery and authentic products. The foundation match was perfect on first try.",
35
+ },
36
+ {
37
+ id: "4",
38
+ name: "Sneha P.",
39
+ rating: 5,
40
+ product: "Kajal",
41
+ text: "Smudge-proof kajal that glides on smoothly. Already repurchased twice.",
42
+ },
43
+ {
44
+ id: "5",
45
+ name: "Divya M.",
46
+ rating: 5,
47
+ product: "Hydrating Moisturizer",
48
+ text: "Finally a moisturizer that works under makeup. Skin looks dewy, not greasy.",
49
+ },
50
+ {
51
+ id: "6",
52
+ name: "Kavya T.",
53
+ rating: 4,
54
+ product: "Eyeshadow Palette",
55
+ text: "Beautiful pigment and minimal fallout. The rose-gold shades are stunning for evenings.",
56
+ },
57
+ ]
58
+
59
+ function Stars({ rating, large = false }: { rating: number; large?: boolean }) {
60
+ return (
61
+ <div
62
+ className={`beauty-reviews__stars${large ? " beauty-reviews__stars--lg" : ""}`}
63
+ aria-label={`${rating} out of 5 stars`}
64
+ >
65
+ {Array.from({ length: 5 }, (_, index) => (
66
+ <span
67
+ key={index}
68
+ className={
69
+ index < rating ? "beauty-reviews__star beauty-reviews__star--filled" : "beauty-reviews__star"
70
+ }
71
+ >
72
+
73
+ </span>
74
+ ))}
75
+ </div>
76
+ )
77
+ }
78
+
79
+ function ReviewCard({ review }: { review: ReviewItem }) {
80
+ return (
81
+ <article className="beauty-reviews__card">
82
+ <span className="beauty-reviews__quote-mark" aria-hidden="true">
83
+ &ldquo;
84
+ </span>
85
+ {review.product ? <span className="beauty-reviews__product">{review.product}</span> : null}
86
+ <p className="beauty-reviews__quote">{review.text}</p>
87
+ <div className="beauty-reviews__card-footer">
88
+ <Stars rating={review.rating} />
89
+ <p className="beauty-reviews__meta">
90
+ <span className="beauty-reviews__name">{review.name}</span>
91
+ <span className="beauty-reviews__dot" aria-hidden="true">
92
+ ·
93
+ </span>
94
+ <span className="beauty-reviews__verified">Verified buyer</span>
95
+ </p>
96
+ </div>
97
+ </article>
98
+ )
99
+ }
100
+
101
+ export default function BeautyReviews({
102
+ eyebrow = "Real reviews",
103
+ title = "Loved by beauty enthusiasts",
104
+ description = "Discover why thousands trust Lumière for authentic makeup, skincare, and everyday glow essentials.",
105
+ aggregateRating = "4.9",
106
+ aggregateLabel = "12,000+ happy customers",
107
+ reviews = DEFAULT_REVIEWS,
108
+ }: {
109
+ eyebrow?: string
110
+ title?: string
111
+ description?: string
112
+ aggregateRating?: string
113
+ aggregateLabel?: string
114
+ reviews?: ReviewItem[]
115
+ }) {
116
+ const items = reviews.filter((review) => review.text?.trim() && review.name?.trim())
117
+ const trackRef = useRef<HTMLDivElement>(null)
118
+ const [canScrollPrev, setCanScrollPrev] = useState(false)
119
+ const [canScrollNext, setCanScrollNext] = useState(true)
120
+
121
+ const updateScrollState = useCallback(() => {
122
+ const track = trackRef.current
123
+ if (!track) return
124
+ const maxScroll = track.scrollWidth - track.clientWidth
125
+ setCanScrollPrev(track.scrollLeft > 8)
126
+ setCanScrollNext(track.scrollLeft < maxScroll - 8)
127
+ }, [])
128
+
129
+ const scrollByCard = useCallback((direction: -1 | 1) => {
130
+ const track = trackRef.current
131
+ if (!track) return
132
+ const card = track.querySelector<HTMLElement>(".beauty-reviews__card")
133
+ const gap = 16
134
+ const amount = (card?.offsetWidth ?? 300) + gap
135
+ track.scrollBy({ left: direction * amount, behavior: "smooth" })
136
+ }, [])
137
+
138
+ useEffect(() => {
139
+ updateScrollState()
140
+ const track = trackRef.current
141
+ if (!track) return
142
+ const observer = new ResizeObserver(() => updateScrollState())
143
+ observer.observe(track)
144
+ return () => observer.disconnect()
145
+ }, [items.length, updateScrollState])
146
+
147
+ if (items.length === 0) return null
148
+
149
+ return (
150
+ <section className="beauty-reviews" aria-labelledby="beauty-reviews-title">
151
+ <div className="beauty-reviews__inner">
152
+ <header className="beauty-reviews__header">
153
+ <div className="beauty-reviews__header-copy">
154
+ {eyebrow ? <p className="beauty-reviews__eyebrow">{eyebrow}</p> : null}
155
+ <h2 id="beauty-reviews-title" className="beauty-reviews__title">
156
+ {title}
157
+ </h2>
158
+ {description ? <p className="beauty-reviews__description">{description}</p> : null}
159
+ </div>
160
+
161
+ <div className="beauty-reviews__aggregate-card">
162
+ <span className="beauty-reviews__score">{aggregateRating}</span>
163
+ <div className="beauty-reviews__aggregate-meta">
164
+ <Stars rating={5} large />
165
+ {aggregateLabel ? <p className="beauty-reviews__aggregate-label">{aggregateLabel}</p> : null}
166
+ </div>
167
+ </div>
168
+ </header>
169
+
170
+ <div className="beauty-reviews__carousel">
171
+ <button
172
+ type="button"
173
+ className="beauty-reviews__nav beauty-reviews__nav--prev"
174
+ onClick={() => scrollByCard(-1)}
175
+ disabled={!canScrollPrev}
176
+ aria-label="Previous reviews"
177
+ >
178
+
179
+ </button>
180
+
181
+ <div className="beauty-reviews__track-wrap">
182
+ <div
183
+ ref={trackRef}
184
+ className="beauty-reviews__track"
185
+ onScroll={updateScrollState}
186
+ tabIndex={0}
187
+ role="region"
188
+ aria-label="Customer reviews"
189
+ >
190
+ {items.map((review) => (
191
+ <ReviewCard key={review.id} review={review} />
192
+ ))}
193
+ </div>
194
+ </div>
195
+
196
+ <button
197
+ type="button"
198
+ className="beauty-reviews__nav beauty-reviews__nav--next"
199
+ onClick={() => scrollByCard(1)}
200
+ disabled={!canScrollNext}
201
+ aria-label="Next reviews"
202
+ >
203
+
204
+ </button>
205
+ </div>
206
+
207
+ <p className="beauty-reviews__scroll-hint">Swipe or use arrows to explore more reviews</p>
208
+ </div>
209
+ </section>
210
+ )
211
+ }