Skip to content

Commit 1e9da61

Browse files
committed
added newsletter cta
1 parent e40411f commit 1e9da61

File tree

3 files changed

+238
-6
lines changed

3 files changed

+238
-6
lines changed

app/(marketing)/page.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
"use client";
2+
3+
import { JoinNewsletter } from "@/components";
14
import {
25
Accordion,
36
Avatar,
@@ -50,7 +53,7 @@ export default function Home() {
5053
<div className="grid gird-cols-1 lg:grid-cols-2 items-center">
5154
<div>
5255
<H3>Why Use RetroUI?</H3>
53-
<H1 className="mt-4">Coz We C00l!!</H1>
56+
<H1 className="mt-4">Coz We C000l!!</H1>
5457
</div>
5558

5659
<img
@@ -104,7 +107,7 @@ export default function Home() {
104107
</div>
105108
</section>
106109

107-
<section className="container max-w-6xl mx-auto ">
110+
<section className="container max-w-6xl mx-auto">
108111
<H2 className="mb-16">
109112
And NO! We didn't just copy Gumroad!
110113
<br />
@@ -120,18 +123,19 @@ export default function Home() {
120123
<H3 className="lg:hidden mt-2 mb-12">👆 RetroUI Card</H3>
121124

122125
<div className="hidden lg:block space-y-4">
123-
<H3>👈 RetroUI Card</H3>
124-
<H3>Gumroad's Card 👉</H3>
126+
<H2 className="text-left">👈 RetroUI Card</H2>
127+
<H2 className="text-right">Gumroad's Card 👉</H2>
125128
</div>
126129
<img
127130
src="/images/gumroad_product_card.png"
128131
alt="our product card"
129-
className="w-80"
132+
className="w-72 ml-auto"
130133
/>
131134
<H3 className="lg:hidden mt-2">👆 Gumroad's Card</H3>
132135
</div>
133136
</section>
134137

138+
<JoinNewsletter />
135139
<footer className="bg-black py-8">
136140
<div className="container max-w-6xl mx-auto flex flex-col lg:flex-row space-y-4 lg:space-y-0 justify-between items-center">
137141
<div className="flex justify-center space-x-4">

components/JoinNewsletter.tsx

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import { Button, H2, Input } from "@/packages/ui";
2+
import { useState } from "react";
3+
4+
const INIT = "INIT";
5+
const SUBMITTING = "SUBMITTING";
6+
const ERROR = "ERROR";
7+
const SUCCESS = "SUCCESS";
8+
const formStates = [INIT, SUBMITTING, ERROR, SUCCESS] as const;
9+
const formStyles = {
10+
id: "cltl71xnw000ris85z92rosvh",
11+
name: "Default",
12+
formStyle: "inline",
13+
placeholderText: "you@example.com",
14+
formFont: "font-sans",
15+
formFontColor: "#000000",
16+
formFontSizePx: 14,
17+
buttonText: "Subscribe",
18+
buttonFont: "font-sans",
19+
buttonFontColor: "#ffffff",
20+
buttonColor: "#000000",
21+
buttonFontSizePx: 14,
22+
successMessage: "Thanks! We'll be in touch!",
23+
successFont: "Inter",
24+
successFontColor: "#000000",
25+
successFontSizePx: 14,
26+
userGroup: "",
27+
};
28+
const domain = "app.loops.so";
29+
30+
export function JoinNewsletter() {
31+
const [email, setEmail] = useState("");
32+
const [formState, setFormState] = useState<(typeof formStates)[number]>(INIT);
33+
const [errorMessage, setErrorMessage] = useState("");
34+
35+
const resetForm = () => {
36+
setEmail("");
37+
setFormState(INIT);
38+
setErrorMessage("");
39+
};
40+
41+
/**
42+
* Rate limit the number of submissions allowed
43+
* @returns {boolean} true if the form has been successfully submitted in the past minute
44+
*/
45+
const hasRecentSubmission = () => {
46+
const time = new Date();
47+
const timestamp = time.valueOf();
48+
const previousTimestamp = localStorage.getItem("loops-form-timestamp");
49+
50+
// Indicate if the last sign up was less than a minute ago
51+
if (
52+
previousTimestamp &&
53+
Number(previousTimestamp) + 60 * 1000 > timestamp
54+
) {
55+
setFormState(ERROR);
56+
setErrorMessage("Too many signups, please try again in a little while");
57+
return true;
58+
}
59+
60+
localStorage.setItem("loops-form-timestamp", timestamp.toString());
61+
return false;
62+
};
63+
64+
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
65+
// Prevent the default form submission
66+
event.preventDefault();
67+
68+
// boundary conditions for submission
69+
if (formState !== INIT) return;
70+
if (!isValidEmail(email)) {
71+
setFormState(ERROR);
72+
setErrorMessage("Please enter a valid email");
73+
return;
74+
}
75+
if (hasRecentSubmission()) return;
76+
setFormState(SUBMITTING);
77+
78+
// build body
79+
const formBody = `userGroup=${encodeURIComponent(
80+
formStyles.userGroup
81+
)}&email=${encodeURIComponent(email)}&mailingLists=`;
82+
83+
// API request to add user to newsletter
84+
fetch(`https://${domain}/api/newsletter-form/${formStyles.id}`, {
85+
method: "POST",
86+
body: formBody,
87+
headers: {
88+
"Content-Type": "application/x-www-form-urlencoded",
89+
},
90+
})
91+
.then((res: any) => [res.ok, res.json(), res])
92+
.then(([ok, dataPromise, res]) => {
93+
if (ok) {
94+
resetForm();
95+
setFormState(SUCCESS);
96+
} else {
97+
dataPromise.then((data: any) => {
98+
setFormState(ERROR);
99+
setErrorMessage(data.message || res.statusText);
100+
localStorage.setItem("loops-form-timestamp", "");
101+
});
102+
}
103+
})
104+
.catch((error) => {
105+
setFormState(ERROR);
106+
// check for cloudflare error
107+
if (error.message === "Failed to fetch") {
108+
setErrorMessage(
109+
"Too many signups, please try again in a little while"
110+
);
111+
} else if (error.message) {
112+
setErrorMessage(error.message);
113+
}
114+
localStorage.setItem("loops-form-timestamp", "");
115+
});
116+
};
117+
118+
const isInline = formStyles.formStyle === "inline";
119+
120+
switch (formState) {
121+
case SUCCESS:
122+
return (
123+
<div
124+
style={{
125+
display: "flex",
126+
alignItems: "center",
127+
justifyContent: "center",
128+
width: "100%",
129+
}}
130+
>
131+
<p
132+
style={{
133+
fontFamily: `'${formStyles.successFont}', sans-serif`,
134+
color: formStyles.successFontColor,
135+
fontSize: `${formStyles.successFontSizePx}px`,
136+
}}
137+
>
138+
{formStyles.successMessage}
139+
</p>
140+
</div>
141+
);
142+
case ERROR:
143+
return (
144+
<>
145+
<SignUpFormError />
146+
<BackButton />
147+
</>
148+
);
149+
default:
150+
return (
151+
<div className="w-full max-w-6xl mx-auto py-16 px-4 border-2 border-black">
152+
<div className="flex flex-col justify-center items-center text-center">
153+
<H2>Join Our Newsletter</H2>
154+
<p className="text-lg text-muted mb-8">
155+
Get notified about latest updates and new launches.
156+
</p>
157+
158+
<form
159+
onSubmit={handleSubmit}
160+
className="flex space-x-2 w-full lg:w-1/2"
161+
>
162+
<Input
163+
placeholder={formStyles.placeholderText}
164+
value={email}
165+
type="text"
166+
name="email"
167+
required={true}
168+
onChange={(e) => setEmail(e.target.value)}
169+
/>
170+
171+
<Button>Subscribe</Button>
172+
</form>
173+
</div>
174+
</div>
175+
);
176+
}
177+
178+
function SignUpFormError() {
179+
return (
180+
<div
181+
style={{
182+
alignItems: "center",
183+
justifyContent: "center",
184+
width: "100%",
185+
}}
186+
>
187+
<p
188+
style={{
189+
fontFamily: "Inter, sans-serif",
190+
color: "rgb(185, 28, 28)",
191+
fontSize: "14px",
192+
}}
193+
>
194+
{errorMessage || "Oops! Something went wrong, please try again"}
195+
</p>
196+
</div>
197+
);
198+
}
199+
200+
function BackButton() {
201+
const [isHovered, setIsHovered] = useState(false);
202+
203+
return (
204+
<button
205+
style={{
206+
color: "#6b7280",
207+
font: "14px, Inter, sans-serif",
208+
margin: "10px auto",
209+
textAlign: "center",
210+
background: "transparent",
211+
border: "none",
212+
cursor: "pointer",
213+
textDecoration: isHovered ? "underline" : "none",
214+
}}
215+
onMouseOut={() => setIsHovered(false)}
216+
onMouseOver={() => setIsHovered(true)}
217+
onClick={resetForm}
218+
>
219+
&larr; Back
220+
</button>
221+
);
222+
}
223+
}
224+
225+
function isValidEmail(email: any) {
226+
return /.+@.+/.test(email);
227+
}

components/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export * from "./ComponentShowcase"
1+
export * from "./ComponentShowcase";
2+
export * from "./JoinNewsletter";

0 commit comments

Comments
 (0)