Skip to content

Commit a98b071

Browse files
authored
Merge pull request #255 from codeunia-dev/feat/newsletter
feat(newsletter): Add comprehensive newsletter management system
2 parents 3a6bbac + dee7024 commit a98b071

File tree

7 files changed

+862
-14
lines changed

7 files changed

+862
-14
lines changed

app/admin/newsletter/page.tsx

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
"use client"
2+
3+
import { useEffect, useState } from "react"
4+
import { Button } from "@/components/ui/button"
5+
import { Input } from "@/components/ui/input"
6+
import { Download, Send, Loader2, Users, Mail } from "lucide-react"
7+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
8+
9+
interface Subscriber {
10+
id: number
11+
email: string
12+
status: string
13+
created_at: string
14+
}
15+
16+
export default function NewsletterAdminPage() {
17+
const [subscribers, setSubscribers] = useState<Subscriber[]>([])
18+
const [loading, setLoading] = useState(true)
19+
const [stats, setStats] = useState({ total: 0, subscribed: 0, unsubscribed: 0 })
20+
const [error, setError] = useState("")
21+
22+
// Newsletter sending state
23+
const [subject, setSubject] = useState("")
24+
const [content, setContent] = useState("")
25+
const [sending, setSending] = useState(false)
26+
27+
useEffect(() => {
28+
fetchSubscribers()
29+
}, [])
30+
31+
const fetchSubscribers = async () => {
32+
try {
33+
const response = await fetch("/api/admin/newsletter/subscribers")
34+
const data = await response.json()
35+
36+
if (response.ok) {
37+
setSubscribers(data.subscribers || [])
38+
setStats(data.stats || { total: 0, subscribed: 0, unsubscribed: 0 })
39+
} else {
40+
setError(data.error || "Failed to fetch subscribers")
41+
console.error("API Error:", data)
42+
}
43+
} catch (err) {
44+
console.error("Failed to fetch subscribers:", err)
45+
setError("Network error - check console")
46+
} finally {
47+
setLoading(false)
48+
}
49+
}
50+
51+
const exportSubscribers = () => {
52+
const csv = [
53+
["Email", "Status", "Subscribed Date"],
54+
...subscribers.map(sub => [
55+
sub.email,
56+
sub.status,
57+
new Date(sub.created_at).toLocaleDateString()
58+
])
59+
].map(row => row.join(",")).join("\n")
60+
61+
const blob = new Blob([csv], { type: "text/csv" })
62+
const url = window.URL.createObjectURL(blob)
63+
const a = document.createElement("a")
64+
a.href = url
65+
a.download = `newsletter-subscribers-${new Date().toISOString().split("T")[0]}.csv`
66+
a.click()
67+
}
68+
69+
const sendNewsletter = async () => {
70+
if (!subject || !content) {
71+
alert("Please fill in both subject and content")
72+
return
73+
}
74+
75+
if (!confirm(`Send newsletter to ${stats.subscribed} subscribers?`)) {
76+
return
77+
}
78+
79+
setSending(true)
80+
try {
81+
const response = await fetch("/api/admin/newsletter/send", {
82+
method: "POST",
83+
headers: { "Content-Type": "application/json" },
84+
body: JSON.stringify({ subject, content }),
85+
})
86+
87+
const data = await response.json()
88+
89+
if (response.ok) {
90+
alert(`Newsletter sent successfully to ${data.sent} subscribers!`)
91+
setSubject("")
92+
setContent("")
93+
} else {
94+
alert(`Failed to send newsletter: ${data.error}`)
95+
}
96+
} catch {
97+
alert("Failed to send newsletter")
98+
} finally {
99+
setSending(false)
100+
}
101+
}
102+
103+
if (loading) {
104+
return (
105+
<div className="flex items-center justify-center min-h-screen">
106+
<Loader2 className="h-8 w-8 animate-spin" />
107+
</div>
108+
)
109+
}
110+
111+
if (error) {
112+
return (
113+
<div className="flex items-center justify-center min-h-screen">
114+
<div className="bg-red-500/10 border border-red-500 rounded-lg p-6 max-w-md">
115+
<h2 className="text-xl font-bold text-red-500 mb-2">Error Loading Subscribers</h2>
116+
<p className="text-foreground/70">{error}</p>
117+
<p className="text-sm text-foreground/50 mt-4">
118+
Make sure you're logged in as an admin and RLS policies are configured correctly.
119+
</p>
120+
</div>
121+
</div>
122+
)
123+
}
124+
125+
return (
126+
<div className="container mx-auto p-6 max-w-6xl">
127+
<div className="space-y-6">
128+
<div>
129+
<h1 className="text-3xl font-bold">Newsletter Management</h1>
130+
<p className="text-foreground/70">Manage subscribers and send newsletters</p>
131+
</div>
132+
133+
{/* Stats */}
134+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
135+
<div className="bg-card border border-border rounded-lg p-6">
136+
<div className="flex items-center justify-between">
137+
<div>
138+
<p className="text-sm text-foreground/70">Total Subscribers</p>
139+
<p className="text-3xl font-bold">{stats.total}</p>
140+
</div>
141+
<Users className="h-8 w-8 text-primary" />
142+
</div>
143+
</div>
144+
<div className="bg-card border border-border rounded-lg p-6">
145+
<div className="flex items-center justify-between">
146+
<div>
147+
<p className="text-sm text-foreground/70">Active</p>
148+
<p className="text-3xl font-bold text-green-500">{stats.subscribed}</p>
149+
</div>
150+
<Mail className="h-8 w-8 text-green-500" />
151+
</div>
152+
</div>
153+
<div className="bg-card border border-border rounded-lg p-6">
154+
<div className="flex items-center justify-between">
155+
<div>
156+
<p className="text-sm text-foreground/70">Unsubscribed</p>
157+
<p className="text-3xl font-bold text-red-500">{stats.unsubscribed}</p>
158+
</div>
159+
<Mail className="h-8 w-8 text-red-500" />
160+
</div>
161+
</div>
162+
</div>
163+
164+
<Tabs defaultValue="subscribers" className="w-full">
165+
<TabsList>
166+
<TabsTrigger value="subscribers">Subscribers</TabsTrigger>
167+
<TabsTrigger value="send">Send Newsletter</TabsTrigger>
168+
</TabsList>
169+
170+
<TabsContent value="subscribers" className="space-y-4">
171+
<div className="flex justify-between items-center">
172+
<h2 className="text-xl font-semibold">All Subscribers</h2>
173+
<Button onClick={exportSubscribers} variant="outline">
174+
<Download className="h-4 w-4 mr-2" />
175+
Export CSV
176+
</Button>
177+
</div>
178+
179+
<div className="bg-card border border-border rounded-lg overflow-hidden">
180+
<div className="overflow-x-auto">
181+
<table className="w-full">
182+
<thead className="bg-muted">
183+
<tr>
184+
<th className="text-left p-4">Email</th>
185+
<th className="text-left p-4">Status</th>
186+
<th className="text-left p-4">Subscribed Date</th>
187+
</tr>
188+
</thead>
189+
<tbody>
190+
{subscribers.map((sub) => (
191+
<tr key={sub.id} className="border-t border-border">
192+
<td className="p-4">{sub.email}</td>
193+
<td className="p-4">
194+
<span className={`px-2 py-1 rounded text-xs ${
195+
sub.status === "subscribed"
196+
? "bg-green-500/20 text-green-500"
197+
: "bg-red-500/20 text-red-500"
198+
}`}>
199+
{sub.status}
200+
</span>
201+
</td>
202+
<td className="p-4">
203+
{new Date(sub.created_at).toLocaleDateString()}
204+
</td>
205+
</tr>
206+
))}
207+
</tbody>
208+
</table>
209+
</div>
210+
</div>
211+
</TabsContent>
212+
213+
<TabsContent value="send" className="space-y-4">
214+
<div className="bg-card border border-border rounded-lg p-6 space-y-4">
215+
<h2 className="text-xl font-semibold">Compose Newsletter</h2>
216+
217+
<div className="space-y-2">
218+
<label className="text-sm font-medium">Subject</label>
219+
<Input
220+
placeholder="Newsletter subject..."
221+
value={subject}
222+
onChange={(e) => setSubject(e.target.value)}
223+
disabled={sending}
224+
/>
225+
</div>
226+
227+
<div className="space-y-2">
228+
<label className="text-sm font-medium">Content (HTML supported)</label>
229+
<textarea
230+
className="w-full min-h-[300px] p-3 rounded-md border border-border bg-background font-mono text-sm"
231+
placeholder={`<h2 style="color: #667eea; margin-top: 0;">Welcome to Our Newsletter!</h2>
232+
<p>Here's what's new this week:</p>
233+
<ul style="line-height: 1.8;">
234+
<li><strong>New Feature:</strong> Description here</li>
235+
<li><strong>Upcoming Event:</strong> Details here</li>
236+
<li><strong>Community Highlight:</strong> Share achievements</li>
237+
</ul>
238+
<p>Stay tuned for more updates!</p>`}
239+
value={content}
240+
onChange={(e) => setContent(e.target.value)}
241+
disabled={sending}
242+
/>
243+
<p className="text-xs text-foreground/60">
244+
💡 Tip: Use HTML for formatting. The template includes a beautiful header, footer, and social links automatically.
245+
</p>
246+
</div>
247+
248+
<Button
249+
onClick={sendNewsletter}
250+
disabled={sending || !subject || !content}
251+
className="w-full"
252+
>
253+
{sending ? (
254+
<>
255+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
256+
Sending...
257+
</>
258+
) : (
259+
<>
260+
<Send className="h-4 w-4 mr-2" />
261+
Send to {stats.subscribed} Subscribers
262+
</>
263+
)}
264+
</Button>
265+
</div>
266+
</TabsContent>
267+
</Tabs>
268+
</div>
269+
</div>
270+
)
271+
}

0 commit comments

Comments
 (0)