228 lines
6.1 KiB
TypeScript
228 lines
6.1 KiB
TypeScript
"use client";
|
|
|
|
import { Button, Text, Input } from "@/components/retroui";
|
|
import { useState } from "react";
|
|
|
|
const INIT = "INIT";
|
|
const SUBMITTING = "SUBMITTING";
|
|
const ERROR = "ERROR";
|
|
const SUCCESS = "SUCCESS";
|
|
const formStates = [INIT, SUBMITTING, ERROR, SUCCESS] as const;
|
|
const formStyles = {
|
|
id: "cltl71xnw000ris85z92rosvh",
|
|
name: "Default",
|
|
formStyle: "inline",
|
|
placeholderText: "you@example.com",
|
|
formFont: "font-sans",
|
|
formFontColor: "#000000",
|
|
formFontSizePx: 14,
|
|
buttonText: "Subscribe",
|
|
buttonFont: "font-sans",
|
|
buttonFontColor: "#ffffff",
|
|
buttonColor: "#000000",
|
|
buttonFontSizePx: 14,
|
|
successMessage: "Thanks! We'll be in touch!",
|
|
successFont: "Inter",
|
|
successFontColor: "#000000",
|
|
successFontSizePx: 14,
|
|
userGroup: "",
|
|
};
|
|
const domain = "app.loops.so";
|
|
|
|
export function JoinNewsletter() {
|
|
const [email, setEmail] = useState("");
|
|
const [formState, setFormState] = useState<(typeof formStates)[number]>(INIT);
|
|
const [errorMessage, setErrorMessage] = useState("");
|
|
|
|
const resetForm = () => {
|
|
setEmail("");
|
|
setFormState(INIT);
|
|
setErrorMessage("");
|
|
};
|
|
|
|
/**
|
|
* Rate limit the number of submissions allowed
|
|
* @returns {boolean} true if the form has been successfully submitted in the past minute
|
|
*/
|
|
const hasRecentSubmission = () => {
|
|
const time = new Date();
|
|
const timestamp = time.valueOf();
|
|
const previousTimestamp = localStorage.getItem("loops-form-timestamp");
|
|
|
|
// Indicate if the last sign up was less than a minute ago
|
|
if (
|
|
previousTimestamp &&
|
|
Number(previousTimestamp) + 60 * 1000 > timestamp
|
|
) {
|
|
setFormState(ERROR);
|
|
setErrorMessage("Too many signups, please try again in a little while");
|
|
return true;
|
|
}
|
|
|
|
localStorage.setItem("loops-form-timestamp", timestamp.toString());
|
|
return false;
|
|
};
|
|
|
|
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
|
// Prevent the default form submission
|
|
event.preventDefault();
|
|
|
|
// boundary conditions for submission
|
|
if (formState !== INIT) return;
|
|
if (!isValidEmail(email)) {
|
|
setFormState(ERROR);
|
|
setErrorMessage("Please enter a valid email");
|
|
return;
|
|
}
|
|
if (hasRecentSubmission()) return;
|
|
setFormState(SUBMITTING);
|
|
|
|
// build body
|
|
const formBody = `userGroup=${encodeURIComponent(
|
|
formStyles.userGroup,
|
|
)}&email=${encodeURIComponent(email)}&mailingLists=`;
|
|
|
|
// API request to add user to newsletter
|
|
fetch(`https://${domain}/api/newsletter-form/${formStyles.id}`, {
|
|
method: "POST",
|
|
body: formBody,
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
})
|
|
.then((res: any) => [res.ok, res.json(), res])
|
|
.then(([ok, dataPromise, res]) => {
|
|
if (ok) {
|
|
resetForm();
|
|
setFormState(SUCCESS);
|
|
} else {
|
|
dataPromise.then((data: any) => {
|
|
setFormState(ERROR);
|
|
setErrorMessage(data.message || res.statusText);
|
|
localStorage.setItem("loops-form-timestamp", "");
|
|
});
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
setFormState(ERROR);
|
|
// check for cloudflare error
|
|
if (error.message === "Failed to fetch") {
|
|
setErrorMessage(
|
|
"Too many signups, please try again in a little while",
|
|
);
|
|
} else if (error.message) {
|
|
setErrorMessage(error.message);
|
|
}
|
|
localStorage.setItem("loops-form-timestamp", "");
|
|
});
|
|
};
|
|
|
|
switch (formState) {
|
|
case SUCCESS:
|
|
return (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
width: "100%",
|
|
}}
|
|
>
|
|
<p
|
|
style={{
|
|
fontFamily: `'${formStyles.successFont}', sans-serif`,
|
|
color: formStyles.successFontColor,
|
|
fontSize: `${formStyles.successFontSizePx}px`,
|
|
}}
|
|
>
|
|
{formStyles.successMessage}
|
|
</p>
|
|
</div>
|
|
);
|
|
case ERROR:
|
|
return (
|
|
<>
|
|
<SignUpFormError />
|
|
<BackButton />
|
|
</>
|
|
);
|
|
default:
|
|
return (
|
|
<div className="w-full max-w-6xl mx-auto py-16 px-4 border-2 border-black">
|
|
<div className="flex flex-col justify-center items-center text-center">
|
|
<Text as="h2">Join Our Newsletter</Text>
|
|
<p className="text-lg text-muted-foreground mb-8">
|
|
Get notified about latest updates and new launches.
|
|
</p>
|
|
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
className="flex space-x-2 w-full lg:w-1/2"
|
|
>
|
|
<Input
|
|
placeholder={formStyles.placeholderText}
|
|
value={email}
|
|
type="text"
|
|
name="email"
|
|
required={true}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
/>
|
|
|
|
<Button>Subscribe</Button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SignUpFormError() {
|
|
return (
|
|
<div
|
|
style={{
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
width: "100%",
|
|
}}
|
|
>
|
|
<p
|
|
style={{
|
|
fontFamily: "Inter, sans-serif",
|
|
color: "rgb(185, 28, 28)",
|
|
fontSize: "14px",
|
|
}}
|
|
>
|
|
{errorMessage || "Oops! Something went wrong, please try again"}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BackButton() {
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
|
|
return (
|
|
<button
|
|
style={{
|
|
color: "#6b7280",
|
|
font: "14px, Inter, sans-serif",
|
|
margin: "10px auto",
|
|
textAlign: "center",
|
|
background: "transparent",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
textDecoration: isHovered ? "underline" : "none",
|
|
}}
|
|
onMouseOut={() => setIsHovered(false)}
|
|
onMouseOver={() => setIsHovered(true)}
|
|
onClick={resetForm}
|
|
>
|
|
← Back
|
|
</button>
|
|
);
|
|
}
|
|
}
|
|
|
|
function isValidEmail(email: any) {
|
|
return /.+@.+/.test(email);
|
|
}
|