Skip to content

Commit 4dc5910

Browse files
authored
Merge pull request #3186 from gluestack/newletter
Newletter
2 parents c7274b9 + 62ea78b commit 4dc5910

File tree

4 files changed

+323
-24
lines changed

4 files changed

+323
-24
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// app/api/listmonk/route.ts
2+
export async function POST(request: Request) {
3+
try {
4+
const body = await request.json();
5+
const { email, name } = body;
6+
7+
if (!email) {
8+
return Response.json(
9+
{ message: 'Email is required' },
10+
{ status: 400 }
11+
);
12+
}
13+
const listmonkConfig = {
14+
baseUrl: process.env.LISTMONK_BASE_URL || 'http://localhost:9000',
15+
username: process.env.LISTMONK_API_USERNAME || 'api_username',
16+
accessToken: process.env.LISTMONK_ACCESS_TOKEN || 'access_token',
17+
defaultListId: parseInt(process.env.LISTMONK_DEFAULT_LIST_ID||'1'),
18+
};
19+
20+
const subscriberData = {
21+
email: email,
22+
name: name || '',
23+
status: 'enabled',
24+
lists: [listmonkConfig.defaultListId],
25+
};
26+
27+
const credentials = Buffer.from(
28+
`${listmonkConfig.username}:${listmonkConfig.accessToken}`
29+
).toString('base64');
30+
31+
const response = await fetch(`${listmonkConfig.baseUrl}/api/subscribers`, {
32+
method: 'POST',
33+
headers: {
34+
'Content-Type': 'application/json',
35+
'Authorization': `Basic ${credentials}`,
36+
},
37+
body: JSON.stringify(subscriberData),
38+
});
39+
40+
const responseText = await response.text();
41+
42+
if (!response.ok) {
43+
console.error("❌ Listmonk error:", responseText);
44+
45+
// Check if it's a duplicate email error
46+
if (responseText.includes('already exists') || responseText.includes('duplicate')) {
47+
return Response.json(
48+
{ message: 'Email is already subscribed' },
49+
{ status: 409 }
50+
);
51+
}
52+
53+
throw new Error(`Listmonk API error: ${response.status} - ${responseText}`);
54+
}
55+
56+
const result = JSON.parse(responseText);
57+
console.log("✅ Successfully added subscriber:", result.data?.email);
58+
59+
return Response.json({
60+
message: 'Successfully subscribed to newsletter',
61+
data: result.data
62+
});
63+
64+
} catch (error: any) {
65+
console.error('❌ Failed to add subscriber:', error);
66+
67+
return Response.json(
68+
{ message: 'Failed to subscribe. Please try again.' },
69+
{ status: 500 }
70+
);
71+
}
72+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { Box } from '@/components/ui/box';
5+
import { ArrowRightIcon, MailIcon } from '@/components/ui/icon';
6+
import { Heading } from '@/components/ui/heading';
7+
import { Text } from '@/components/ui/text';
8+
import { Input, InputIcon, InputField } from '@/components/ui/input';
9+
import { Button, ButtonText, ButtonIcon } from '@/components/ui/button';
10+
import {
11+
Avatar,
12+
AvatarFallbackText,
13+
AvatarGroup,
14+
} from '@/components/ui/avatar';
15+
import { Link } from '@/components/ui/link';
16+
import { VStack } from '@/components/ui/vstack';
17+
import {
18+
Modal,
19+
ModalBackdrop,
20+
ModalContent,
21+
ModalCloseButton,
22+
ModalHeader,
23+
ModalBody,
24+
} from '@/components/ui/modal';
25+
import { Icon, CloseIcon } from '@/components/ui/icon';
26+
import axios from 'axios';
27+
import Image from 'next/image';
28+
29+
30+
const emailValidator =
31+
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
32+
33+
interface NewsletterModalProps {
34+
showModal: boolean;
35+
setShowModal: (show: boolean) => void;
36+
37+
}
38+
39+
export default function NewsletterModal({
40+
showModal,
41+
setShowModal,
42+
43+
}: NewsletterModalProps) {
44+
const [loading, setLoading] = useState(false);
45+
const [, setError] = useState(false);
46+
const [success, setSuccess] = useState(false);
47+
const [email, setEmail] = useState<string>('');
48+
const [errorMessage, setErrorMessage] = useState('');
49+
50+
const makeRequestToServer = async () => {
51+
try {
52+
setLoading(true);
53+
setError(false);
54+
setErrorMessage('');
55+
56+
const response = await axios.post('/api/listmonk', {
57+
email: email,
58+
name: ''
59+
});
60+
61+
if (response.status === 200) {
62+
setSuccess(true);
63+
setError(false);
64+
setErrorMessage('');
65+
setEmail('');
66+
}
67+
} catch (error: any) {
68+
setError(true);
69+
setSuccess(false);
70+
71+
if (error.response?.status === 409) {
72+
setErrorMessage('This email is already subscribed!');
73+
} else if (error.response?.status === 400) {
74+
setErrorMessage('Please enter a valid email address!');
75+
} else {
76+
setErrorMessage(error.response?.data?.message || 'Error subscribing to newsletter. Please try again!');
77+
}
78+
79+
setEmail('');
80+
} finally {
81+
setLoading(false);
82+
}
83+
};
84+
85+
const subscribeToNewsLetter = (e: any) => {
86+
e.preventDefault();
87+
if (email === '') {
88+
setErrorMessage('Email address is required!');
89+
setError(true);
90+
} else if (!emailValidator.test(email)) {
91+
setErrorMessage('Enter a valid email address!');
92+
setError(true);
93+
} else {
94+
makeRequestToServer();
95+
}
96+
};
97+
98+
const handleClose = () => {
99+
setShowModal(false);
100+
// Reset form state when closing
101+
setEmail('');
102+
setSuccess(false);
103+
setError(false);
104+
setErrorMessage('');
105+
};
106+
107+
return (
108+
<Modal isOpen={showModal} onClose={handleClose} size="lg">
109+
<ModalBackdrop />
110+
<ModalContent className="max-w-2xl">
111+
<ModalHeader>
112+
<ModalCloseButton>
113+
<Icon as={CloseIcon} />
114+
</ModalCloseButton>
115+
</ModalHeader>
116+
<ModalBody>
117+
<Box className="p-6">
118+
<VStack className="gap-3">
119+
<Heading className="text-3xl font-bold leading-9 mb-3 text-typography-900">
120+
Get exclusive updates!
121+
</Heading>
122+
<Text className="text-lg font-normal leading-[30px]">
123+
We can&apos;t do this alone, we would love feedback and the
124+
fastest way for us to reach out to you is via emails. We
125+
won&apos;t spam, I promise!
126+
</Text>
127+
<Link
128+
className="text-lg font-bold underline underline-offset-4"
129+
href="https://gluestack.io/support"
130+
isExternal
131+
>
132+
Learn more
133+
</Link>
134+
</VStack>
135+
136+
<Box className="flex-col items-center md:flex-row md:items-start md:w-full mt-8">
137+
<Input
138+
size="sm"
139+
isReadOnly={success}
140+
className="flex-row px-3 py-2 flex-1 mb-4 w-full md:mb-0"
141+
>
142+
<InputIcon as={MailIcon} />
143+
<InputField
144+
onKeyPress={(e: any) => {
145+
if (e.key === 'Enter') {
146+
subscribeToNewsLetter(e);
147+
}
148+
}}
149+
onChangeText={(value: string) => {
150+
setEmail(value);
151+
}}
152+
value={email}
153+
aria-label="email"
154+
placeholder="Enter your email"
155+
/>
156+
</Input>
157+
{!success && (
158+
<Button
159+
size="sm"
160+
isDisabled={success || loading}
161+
className="w-full ml-0 md:w-auto md:ml-3"
162+
onPress={subscribeToNewsLetter}
163+
>
164+
{loading ? (
165+
<ButtonText>Loading...</ButtonText>
166+
) : (
167+
<>
168+
<ButtonText className="font-medium leading-normal">
169+
Subscribe
170+
</ButtonText>
171+
<ButtonIcon
172+
className="w-[18px] h-[18px]"
173+
as={ArrowRightIcon}
174+
/>
175+
</>
176+
)}
177+
</Button>
178+
)}
179+
</Box>
180+
181+
<Box className="mt-4">
182+
{success && (
183+
<Text className="text-green-500 mt-1">
184+
Thank you for subscribing to our newsletter!
185+
</Text>
186+
)}
187+
{errorMessage && (
188+
<Text className="text-red-500">{errorMessage}</Text>
189+
)}
190+
</Box>
191+
192+
</Box>
193+
</ModalBody>
194+
</ModalContent>
195+
</Modal>
196+
);
197+
}

apps/website/components/page-components/header/index.tsx

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import React, { useContext, useState } from 'react';
44
import { Badge, BadgeText } from '@/components/ui/badge';
55
import { Divider } from '@/components/ui/divider';
66
import { HStack } from '@/components/ui/hstack';
7-
import { Icon, ChevronUpIcon, ChevronDownIcon } from '@/components/ui/icon';
7+
import { Icon, ChevronUpIcon, ChevronDownIcon,CloseIcon } from '@/components/ui/icon';
88
import { Link } from '@/components/ui/link';
99
import { Pressable } from '@/components/ui/pressable';
1010
import { Text } from '@/components/ui/text';
@@ -16,6 +16,7 @@ import GluestackLogo from '@/public/svg/gluestack_logo.svg';
1616
import GluestackLogoDark from '@/public/svg/gluestack_logo_dark.svg';
1717
import { MenuIcon, MoonIcon, SunIcon, X } from 'lucide-react-native';
1818

19+
import NewsletterModal from './NewsLetterModal';
1920
import AppLaunchKitLogo from '@/public/icon/logo/app-launch-kit/dark-mode.svg';
2021
import GluestackProLogo from '@/public/icon/logo/gluestack-pro/logo.svg';
2122
import AppLaunchKitLogoDark from '@/public/icon/logo/app-launch-kit/light-mode.svg';
@@ -30,6 +31,7 @@ import { ThemeContext } from '@/utils/context/theme-context';
3031
import { usePathname } from 'next/navigation';
3132
import { UiDocSearch } from './Docsearch';
3233
import { LayoutContext } from '@/components/custom/layout/LayoutContext';
34+
import { Button, ButtonText } from '@/components/ui/button';
3335

3436
// Updated Header component with internal state management
3537
const Header = ({
@@ -63,6 +65,8 @@ const Header = ({
6365
setIsOpenSidebar(!isOpenSidebar);
6466
};
6567

68+
const [showModal, setShowModal] = React.useState(false);
69+
6670
const dropdownOptions = [
6771
{
6872
href: 'https://rapidnative.com/?utm_source=gluestack.io&utm_medium=header&utm_campaign=brand-awareness',
@@ -456,19 +460,28 @@ const Header = ({
456460
href="/ui/docs"
457461
className="bg-primary-500 px-4 py-1.5 xl:flex hidden rounded"
458462
>
459-
<Text className="text-sm text-typography-0">
460-
Get Started
461-
</Text>
463+
<Text className="text-sm text-typography-0">Get Started</Text>
462464
</Link>
463465
) : (
464-
<Link
465-
href="https://rapidnative.com/?utm_source=gluestack.io&utm_medium=banner_docs&utm_campaign=brand-awareness"
466-
className="bg-primary-500 px-4 py-1.5 xl:flex hidden rounded"
467-
>
468-
<Text className="text-sm text-typography-0">
469-
Prompt to React Native UI
470-
</Text>
471-
</Link>
466+
<HStack className="gap-3">
467+
<Link
468+
onPress={() => setShowModal(true)}
469+
className="border border-outline-200 px-4 py-1.5 xl:flex hidden rounded"
470+
>
471+
<Text className="text-sm text-typography-900">
472+
Get Updates
473+
</Text>
474+
</Link>
475+
<NewsletterModal showModal={showModal} setShowModal={setShowModal}/>
476+
<Link
477+
href="https://rapidnative.com/?utm_source=gluestack.io&utm_medium=banner_docs&utm_campaign=brand-awareness"
478+
className="bg-primary-500 px-4 py-1.5 xl:flex hidden rounded"
479+
>
480+
<Text className="text-sm text-typography-0">
481+
Prompt to React Native UI
482+
</Text>
483+
</Link>
484+
</HStack>
472485
)}
473486

474487
{/* Mobile Menu Button */}

0 commit comments

Comments
 (0)