Skip to content

Commit e97a3e5

Browse files
author
Boopathi
committed
Update newsletter feature: new form, types, and dashboard integration
1 parent 2f01f68 commit e97a3e5

File tree

4 files changed

+157
-13
lines changed

4 files changed

+157
-13
lines changed

src/app/types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
21
export type View = "home" | "blog" | "mentors" | "incubators" | "pricing" | "msmes" | "dashboard" | "login" | "signup" | "education" | "contact" | "complete-profile";
3-
export type DashboardTab = "overview" | "msmes" | "incubators" | "mentors" | "submission" | "settings" | "users" | "blog" | "sessions";
2+
export type DashboardTab = "overview" | "msmes" | "incubators" | "mentors" | "submission" | "settings" | "users" | "blog" | "sessions" | "subscribers";
43
export type MentorDashboardTab = "overview" | "mentees" | "schedule" | "profile" | "settings";
54
export type IncubatorDashboardTab = "overview" | "submissions" | "profile" | "settings";
65
export type MsmeDashboardTab = "overview" | "submissions" | "profile" | "settings";
@@ -32,6 +31,12 @@ export type AppUser = {
3231
created_at: string;
3332
};
3433

34+
export type NewsletterSubscriber = {
35+
id: number;
36+
email: string;
37+
subscribed_at: string;
38+
};
39+
3540
export type BlogPost = {
3641
title: string;
3742
image: string;

src/components/layout/footer.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
21
import { Linkedin, Github } from "lucide-react";
32
import { Separator } from "@/components/ui/separator";
3+
import NewsletterForm from "./newsletter-form";
44

55
export default function Footer() {
66
return (
77
<footer className="border-t">
8-
<div className="container mx-auto py-6 px-4">
8+
<div className="container mx-auto py-12 px-4 space-y-12">
9+
<NewsletterForm />
10+
<Separator />
911
<div className="flex flex-col md:flex-row justify-between items-center gap-6 text-sm">
1012
{/* Left: Logo and Tagline */}
11-
<div className="flex-1 flex justify-start">
13+
<div className="flex-1 flex justify-center md:justify-start">
1214
<div className="flex items-center gap-3 text-center md:text-left">
1315
<div className="font-headline text-2xl" style={{ color: '#facc15' }}>
1416
hustl<strong className="text-3xl align-middle font-bold"></strong>p
@@ -30,7 +32,7 @@ export default function Footer() {
3032
</div>
3133

3234
{/* Right: Social Icons */}
33-
<div className="flex-1 flex justify-end">
35+
<div className="flex-1 flex justify-center md:justify-end">
3436
<div className="flex items-center gap-4">
3537
<a href="#" aria-label="X" className="text-muted-foreground hover:text-primary transition-colors">
3638
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 fill-current">
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
2+
"use client";
3+
4+
import { useForm } from "react-hook-form";
5+
import { zodResolver } from "@hookform/resolvers/zod";
6+
import * as z from "zod";
7+
import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form";
8+
import { Input } from "@/components/ui/input";
9+
import { Button } from "@/components/ui/button";
10+
import { useToast } from "@/hooks/use-toast";
11+
import { Loader2, Mail } from "lucide-react";
12+
import { API_BASE_URL } from "@/lib/api";
13+
14+
const newsletterSchema = z.object({
15+
email: z.string().email({ message: "Please enter a valid email address." }),
16+
});
17+
18+
type NewsletterFormValues = z.infer<typeof newsletterSchema>;
19+
20+
export default function NewsletterForm() {
21+
const { toast } = useToast();
22+
const form = useForm<NewsletterFormValues>({
23+
resolver: zodResolver(newsletterSchema),
24+
defaultValues: {
25+
email: "",
26+
},
27+
});
28+
29+
const { formState: { isSubmitting } } = form;
30+
31+
const onSubmit = async (data: NewsletterFormValues) => {
32+
try {
33+
const response = await fetch(`${API_BASE_URL}/api/subscribe-newsletter`, {
34+
method: 'POST',
35+
headers: { 'Content-Type': 'application/json' },
36+
body: JSON.stringify(data),
37+
});
38+
39+
const result = await response.json();
40+
41+
if (response.ok) {
42+
toast({
43+
title: "Subscription Successful!",
44+
description: result.message || "Please check your email to confirm your subscription.",
45+
});
46+
form.reset();
47+
} else {
48+
toast({
49+
variant: "destructive",
50+
title: "Subscription Failed",
51+
description: result.error || "An unknown error occurred.",
52+
});
53+
}
54+
} catch (error) {
55+
toast({
56+
variant: "destructive",
57+
title: "Network Error",
58+
description: "Could not connect to the server. Please try again later.",
59+
});
60+
}
61+
};
62+
63+
return (
64+
<div className="text-center max-w-2xl mx-auto">
65+
<h2 className="text-3xl md:text-4xl font-bold font-headline mb-4">Stay in the loop!</h2>
66+
<p className="text-muted-foreground mb-8">
67+
Turn your idea into impact. Join our waitlist today and get 1 year of free access when we launch!
68+
</p>
69+
<Form {...form}>
70+
<form onSubmit={form.handleSubmit(onSubmit)} className="flex items-start max-w-lg mx-auto gap-2">
71+
<FormField
72+
control={form.control}
73+
name="email"
74+
render={({ field }) => (
75+
<FormItem className="w-full">
76+
<FormControl>
77+
<div className="relative">
78+
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
79+
<Input
80+
type="email"
81+
placeholder="Enter your email address..."
82+
className="pl-10"
83+
{...field}
84+
disabled={isSubmitting}
85+
/>
86+
</div>
87+
</FormControl>
88+
<FormMessage className="text-left" />
89+
</FormItem>
90+
)}
91+
/>
92+
<Button type="submit" disabled={isSubmitting}>
93+
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
94+
Let Me In
95+
</Button>
96+
</form>
97+
</Form>
98+
</div>
99+
);
100+
}

src/components/views/dashboard.tsx

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/
1515
import * as LucideIcons from "lucide-react";
1616
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
1717
import { Badge } from "@/components/ui/badge";
18-
import type { View, DashboardTab, UserRole, AppUser, BlogPost, EducationProgram } from "@/app/types";
18+
import type { View, DashboardTab, UserRole, AppUser, BlogPost, EducationProgram, NewsletterSubscriber } from "@/app/types";
1919
import { ScrollArea } from "@/components/ui/scroll-area";
2020
import { Button } from "@/components/ui/button";
2121
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
@@ -92,6 +92,9 @@ export default function DashboardView({ isOpen, onOpenChange, user, userRole, ha
9292
const [userToDelete, setUserToDelete] = useState<AppUser | null>(null);
9393
const [userToBan, setUserToBan] = useState<AppUser | null>(null);
9494

95+
const [subscribers, setSubscribers] = useState<NewsletterSubscriber[]>([]);
96+
const [isLoadingSubscribers, setIsLoadingSubscribers] = useState(false);
97+
9598
const [blogPosts, setBlogPosts] = useState<BlogPost[]>([]);
9699
const [educationPrograms, setEducationPrograms] = useState<EducationProgram[]>([]);
97100

@@ -126,11 +129,25 @@ export default function DashboardView({ isOpen, onOpenChange, user, userRole, ha
126129
else toast({ variant: 'destructive', title: 'Failed to fetch users' });
127130
} catch (error) { toast({ variant: 'destructive', title: 'Network Error' }); } finally { setIsLoadingUsers(false); }
128131
};
132+
133+
const fetchSubscribers = async () => {
134+
setIsLoadingSubscribers(true);
135+
const token = localStorage.getItem('token');
136+
if (!token) { toast({ variant: 'destructive', title: 'Authentication Error' }); setIsLoadingSubscribers(false); return; }
137+
try {
138+
const response = await fetch(`${API_BASE_URL}/api/subscribers`, { headers: { 'Authorization': `Bearer ${token}` } });
139+
if (response.ok) setSubscribers(await response.json());
140+
else toast({ variant: 'destructive', title: 'Failed to fetch subscribers' });
141+
} catch (error) { toast({ variant: 'destructive', title: 'Network Error' }); } finally { setIsLoadingSubscribers(false); }
142+
};
129143

130144
useEffect(() => {
131-
if (activeTab === 'users' && userRole === 'admin') fetchUsers();
132-
if (activeTab === 'blog' && userRole === 'admin') fetchBlogPosts();
133-
if (activeTab === 'sessions' && userRole === 'admin') fetchEducationPrograms();
145+
if (userRole === 'admin') {
146+
if (activeTab === 'users') fetchUsers();
147+
if (activeTab === 'blog') fetchBlogPosts();
148+
if (activeTab === 'sessions') fetchEducationPrograms();
149+
if (activeTab === 'subscribers') fetchSubscribers();
150+
}
134151
}, [activeTab, userRole]);
135152

136153
const handleApiResponse = async (response: Response, successMessage: string, errorMessage: string) => {
@@ -210,7 +227,7 @@ export default function DashboardView({ isOpen, onOpenChange, user, userRole, ha
210227
}
211228
}
212229

213-
const adminTabs = ["overview", "users", "blog", "sessions", "settings"];
230+
const adminTabs = ["overview", "users", "subscribers", "blog", "sessions", "settings"];
214231
const founderTabs = ["overview", "msmes", "incubators", "mentors", "submission", "settings"];
215232
const availableTabs = userRole === 'admin' ? adminTabs : founderTabs;
216233
const pendingApprovalCount = users.filter(u => !u.is_confirmed).length;
@@ -224,9 +241,9 @@ export default function DashboardView({ isOpen, onOpenChange, user, userRole, ha
224241
</DialogHeader>
225242
<div className="flex-grow flex flex-col min-h-0 p-6 pt-0">
226243
<Tabs value={activeTab} onValueChange={(tab) => setActiveTab(tab as DashboardTab)} className="flex flex-col flex-grow min-h-0">
227-
<TabsList className={userRole === 'admin' ? 'grid w-full grid-cols-5' : 'grid w-full grid-cols-6'}>
244+
<TabsList className={userRole === 'admin' ? 'grid w-full grid-cols-6' : 'grid w-full grid-cols-6'}>
228245
{availableTabs.map(tab => {
229-
const Icon = LucideIcons[tab === 'overview' ? 'LayoutDashboard' : tab === 'msmes' ? 'Briefcase' : tab === 'incubators' ? 'Lightbulb' : tab === 'mentors' ? 'Users' : tab === 'submission' ? 'FileText' : tab === 'settings' ? 'Settings' : tab === 'users' ? 'User' : tab === 'blog' ? 'Newspaper' : 'BookOpen' as keyof typeof LucideIcons] || LucideIcons.HelpCircle;
246+
const Icon = LucideIcons[tab === 'overview' ? 'LayoutDashboard' : tab === 'msmes' ? 'Briefcase' : tab === 'incubators' ? 'Lightbulb' : tab === 'mentors' ? 'Users' : tab === 'submission' ? 'FileText' : tab === 'settings' ? 'Settings' : tab === 'users' ? 'User' : tab === 'subscribers' ? 'Mail' : tab === 'blog' ? 'Newspaper' : 'BookOpen' as keyof typeof LucideIcons] || LucideIcons.HelpCircle;
230247
return (
231248
<TabsTrigger value={tab} key={tab} className="capitalize">
232249
<Icon className="mr-2 h-4 w-4" /> {tab === 'mentors' ? 'My Mentors' : tab}
@@ -254,6 +271,26 @@ export default function DashboardView({ isOpen, onOpenChange, user, userRole, ha
254271
</TableBody></Table>)}
255272
</CardContent></Card>
256273
</TabsContent>
274+
<TabsContent value="subscribers" className="mt-0">
275+
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
276+
<CardHeader><CardTitle>Newsletter Subscribers</CardTitle><CardDescription>List of all users subscribed to the newsletter.</CardDescription></CardHeader>
277+
<CardContent>
278+
{isLoadingSubscribers ? <div className="flex justify-center items-center h-48"><LucideIcons.Loader2 className="h-8 w-8 animate-spin" /></div> : (
279+
<Table><TableHeader><TableRow><TableHead>Email</TableHead><TableHead>Subscribed Date</TableHead></TableRow></TableHeader><TableBody>
280+
{subscribers.map(sub => (
281+
<TableRow key={sub.id}>
282+
<TableCell className="font-medium">{sub.email}</TableCell>
283+
<TableCell>{new Date(sub.subscribed_at).toLocaleDateString()}</TableCell>
284+
</TableRow>
285+
))}
286+
</TableBody></Table>
287+
)}
288+
{subscribers.length === 0 && !isLoadingSubscribers && (
289+
<p className="text-center text-muted-foreground py-8">There are no newsletter subscribers yet.</p>
290+
)}
291+
</CardContent>
292+
</Card>
293+
</TabsContent>
257294
<TabsContent value="blog" className="mt-0 space-y-6">
258295
<Card><CardHeader><CardTitle>Create New Blog Post</CardTitle></CardHeader><CardContent><Form {...blogForm}><form onSubmit={blogForm.handleSubmit(onBlogSubmit)} className="space-y-4"><FormField control={blogForm.control} name="title" render={({ field }) => (<FormItem><FormLabel>Title</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>)}/><FormField control={blogForm.control} name="excerpt" render={({ field }) => (<FormItem><FormLabel>Excerpt</FormLabel><FormControl><Textarea {...field} /></FormControl><FormMessage /></FormItem>)}/><FormField control={blogForm.control} name="content" render={({ field }) => (<FormItem><FormLabel>Content</FormLabel><FormControl><Textarea rows={8} {...field} /></FormControl><FormMessage /></FormItem>)}/><FormField control={blogForm.control} name="image" render={({ field }) => (<FormItem><FormLabel>Image URL</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>)}/><FormField control={blogForm.control} name="hint" render={({ field }) => (<FormItem><FormLabel>Image Hint</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>)}/><Button type="submit">Publish Post</Button></form></Form></CardContent></Card>
259296
</TabsContent>

0 commit comments

Comments
 (0)