Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

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}
>
&larr; Back
</button>
);
}
}
function isValidEmail(email: any) {
return /.+@.+/.test(email);
}