Skip to content

Commit d948588

Browse files
committed
ok
1 parent 3a75e4d commit d948588

5 files changed

Lines changed: 98 additions & 6 deletions

File tree

backend/internal/api/subscribers.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,39 @@ func ListSubscribers(w http.ResponseWriter, r *http.Request) {
281281
var subscribers []models.Subscriber
282282
q.Offset(offset).Limit(limit).Find(&subscribers)
283283

284+
// Enrich with coupon info
285+
subIDs := make([]string, len(subscribers))
286+
for i, s := range subscribers {
287+
subIDs[i] = s.ID
288+
}
289+
var coupons []models.CouponCode
290+
if len(subIDs) > 0 {
291+
database.DB.Where("subscriber_id IN ?", subIDs).Find(&coupons)
292+
}
293+
couponMap := map[string]models.CouponCode{}
294+
for _, c := range coupons {
295+
couponMap[c.SubscriberID] = c
296+
}
297+
298+
type enrichedSub struct {
299+
models.Subscriber
300+
CouponCode string `json:"coupon_code,omitempty"`
301+
CouponStatus string `json:"coupon_status,omitempty"`
302+
PromoUsed string `json:"promo_used,omitempty"`
303+
}
304+
305+
enriched := make([]enrichedSub, len(subscribers))
306+
for i, s := range subscribers {
307+
enriched[i] = enrichedSub{Subscriber: s}
308+
if c, ok := couponMap[s.ID]; ok {
309+
enriched[i].CouponCode = c.Code
310+
enriched[i].CouponStatus = string(c.Status)
311+
enriched[i].PromoUsed = c.SourceCode
312+
}
313+
}
314+
284315
jsonResponse(w, map[string]interface{}{
285-
"subscribers": subscribers,
316+
"subscribers": enriched,
286317
"total": total,
287318
"page": page,
288319
"limit": limit,

docs/public-api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Content-Type: application/json
1515
| `name` | string || Subscriber name |
1616
| `source` | string || `"form"` (default), `"api"`, or `"widget"` |
1717
| `promo` | string || Promo campaign code (e.g., `get5`) |
18+
| `custom_data` | object || Custom field responses. Keys match field `key` from project settings (e.g., `{"city":"NYC","plan":"Pro"}`) |
1819

1920
**Response `201`:**
2021
```json

frontend/src/routes/dashboard.projects.$id.subscribers.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,20 @@ function SubscribersPage() {
124124
<th>Name</th>
125125
<th>Email</th>
126126
<th>Status</th>
127+
<th>Promo</th>
128+
<th>Coupon</th>
127129
<th>Source</th>
130+
<th>IP</th>
128131
<th>Country</th>
129132
<th>Signed Up</th>
130133
<th style={{ width: 60 }}></th>
131134
</tr>
132135
</thead>
133136
<tbody>
134137
{isLoading ? (
135-
<tr><td colSpan={8} style={{ textAlign: 'center', padding: 40, color: '#64748b' }}>Loading...</td></tr>
138+
<tr><td colSpan={11} style={{ textAlign: 'center', padding: 40, color: '#64748b' }}>Loading...</td></tr>
136139
) : subscribers.length === 0 ? (
137-
<tr><td colSpan={8} style={{ textAlign: 'center', padding: 40, color: '#64748b' }}>No subscribers found</td></tr>
140+
<tr><td colSpan={11} style={{ textAlign: 'center', padding: 40, color: '#64748b' }}>No subscribers found</td></tr>
138141
) : subscribers.map((s: any) => (
139142
<React.Fragment key={s.id}>
140143
<tr>
@@ -154,9 +157,31 @@ function SubscribersPage() {
154157
{s.status}
155158
</span>
156159
</td>
160+
<td style={{ fontSize: 13 }}>
161+
{s.promo_used ? (
162+
<span style={{
163+
padding: '2px 8px', borderRadius: 6, fontSize: 11, fontWeight: 600,
164+
background: 'rgba(250,204,21,0.1)', color: '#facc15', border: '1px solid rgba(250,204,21,0.2)',
165+
}}>{s.promo_used}</span>
166+
) : <span style={{ color: '#334155' }}></span>}
167+
</td>
168+
<td style={{ fontSize: 13 }}>
169+
{s.coupon_code ? (
170+
<div>
171+
<code style={{ fontSize: 11, color: '#94a3b8' }}>{s.coupon_code}</code>
172+
<span className={`badge ${s.coupon_status === 'used' ? 'badge-green' : s.coupon_status === 'revoked' ? 'badge-red' : 'badge-gray'}`}
173+
style={{ marginLeft: 4, fontSize: 10 }}>
174+
{s.coupon_status}
175+
</span>
176+
</div>
177+
) : <span style={{ color: '#334155' }}></span>}
178+
</td>
157179
<td>
158180
<span className="badge badge-purple">{s.source}</span>
159181
</td>
182+
<td style={{ fontSize: 12, color: '#475569', fontFamily: 'monospace' }}>
183+
{s.ip_address || '—'}
184+
</td>
160185
<td style={{ fontSize: 13 }}>
161186
{s.country ? (
162187
<span style={{ color: '#94a3b8' }}>{s.country}</span>
@@ -179,7 +204,7 @@ function SubscribersPage() {
179204
if (entries.length === 0) return null
180205
return (
181206
<tr>
182-
<td colSpan={8} style={{ padding: '8px 16px 12px 48px', background: 'rgba(99,102,241,0.03)' }}>
207+
<td colSpan={11} style={{ padding: '8px 16px 12px 48px', background: 'rgba(99,102,241,0.03)' }}>
183208
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px 20px' }}>
184209
{entries.map(([k, v]) => (
185210
<div key={k} style={{ fontSize: 12 }}>

frontend/src/routes/docs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ Content-Type: application/json`}</CodeBlock>
183183
['<code>name</code>', 'string', '✅', 'Subscriber name'],
184184
['<code>source</code>', 'string', '—', '<code>form</code>, <code>api</code>, or <code>widget</code>'],
185185
['<code>promo</code>', 'string', '—', 'Promo campaign trigger code (e.g. <code>get5</code>)'],
186-
['<code>custom_data</code>', 'object', '—', 'Custom field responses as key-value pairs'],
186+
['<code>custom_data</code>', 'object', '—', 'Responses to custom fields. Keys match the field <code>key</code> defined in project settings (e.g. <code>{"city":"NYC","plan":"Pro"}</code>)'],
187187
]}
188188
/>
189189
<CodeBlock>{`// Response 201

frontend/src/routes/w.$slug.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createFileRoute } from '@tanstack/react-router'
22
import { useQuery, useMutation } from '@tanstack/react-query'
33
import { publicApi } from '@/lib/api'
44
import { useState } from 'react'
5-
import { Hourglass, CheckCircle, Mail, User, Zap, Gift, Copy, Check } from 'lucide-react'
5+
import { Hourglass, CheckCircle, Mail, User, Zap, Gift, Copy, Check, Tag } from 'lucide-react'
66
import toast from 'react-hot-toast'
77
import { getDaysLeft } from '@/lib/utils'
88

@@ -16,6 +16,11 @@ function WaitlistPage() {
1616
const [form, setForm] = useState<Record<string, any>>({ name: '', email: '' })
1717
const [coupon, setCoupon] = useState<any>(null)
1818
const [codeCopied, setCodeCopied] = useState(false)
19+
const [showPromo, setShowPromo] = useState(false)
20+
21+
// Read promo code from URL: /w/my-project?promo=get5
22+
const urlPromo = new URLSearchParams(window.location.search).get('promo') || ''
23+
const [promo, setPromo] = useState(urlPromo)
1924

2025
const { data, isLoading, error } = useQuery({
2126
queryKey: ['public-project', slug],
@@ -38,6 +43,7 @@ function WaitlistPage() {
3843
})
3944
return publicApi.subscribe(slug, {
4045
name: form.name, email: form.email,
46+
...(promo ? { promo } : {}),
4147
...(Object.keys(customData).length > 0 ? { custom_data: customData } : {}),
4248
})
4349
},
@@ -247,6 +253,35 @@ function WaitlistPage() {
247253
</div>
248254

249255
{/* Custom fields */}
256+
257+
{/* Promo code */}
258+
{urlPromo ? (
259+
<div style={{
260+
display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px',
261+
background: `rgba(${hexToRgb(themeColor)},0.06)`, borderRadius: 8,
262+
border: `1px solid rgba(${hexToRgb(themeColor)},0.15)`,
263+
}}>
264+
<Tag size={13} color={themeColor} />
265+
<span style={{ fontSize: 13, color: '#94a3b8' }}>Promo: <strong style={{ color: themeColor }}>{urlPromo}</strong></span>
266+
</div>
267+
) : (
268+
<div>
269+
{!showPromo ? (
270+
<button type="button" onClick={() => setShowPromo(true)}
271+
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, color: '#475569', padding: 0 }}>
272+
Have a promo code?
273+
</button>
274+
) : (
275+
<div style={{ position: 'relative' }}>
276+
<Tag size={14} style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)', color: '#475569' }} />
277+
<input className="input" placeholder="Enter promo code"
278+
value={promo} onChange={e => setPromo(e.target.value.toLowerCase())}
279+
style={{ paddingLeft: 34 }} />
280+
</div>
281+
)}
282+
</div>
283+
)}
284+
250285
{customFields.map((cf: any) => (
251286
<div key={cf.key}>
252287
{cf.type === 'text' && (

0 commit comments

Comments
 (0)