Skip to content

Instantly share code, notes, and snippets.

@phil-loops
Last active August 7, 2026 12:03
Show Gist options
  • Select an option

  • Save phil-loops/55548ef8a861f03c778a6c4c898d067a to your computer and use it in GitHub Desktop.

Select an option

Save phil-loops/55548ef8a861f03c778a6c4c898d067a to your computer and use it in GitHub Desktop.
form.tsx
////////////////
//
// Using Framer?
// See https://www.framer.com/learn/code-components/ for more info
//
////////////////
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: "clll2b9kx002xji0owzntsaxw",
name: "Default",
formStyle: "buttonBelow",
placeholderText: "Your mail",
formFont: "Inter",
formFontColor: "#000000",
formFontSizePx: 14,
buttonText: "Subscribe",
buttonFont: "Inter",
buttonFontColor: "#000000",
buttonColor: "#EDEDED",
buttonFontSizePx: 14,
successMessage: "Thanks! We'll be in touch!",
successFont: "Inter",
successFontColor: "#000000",
successFontSizePx: 14,
userGroup: "",
};
const domain = "app.loops.so";
export default function SignUpFormReact() {
const [email, setEmail] = useState("");
const [message, setMessage] = 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)}&favoriteColor=${encodeURIComponent(
message
)}`;
// 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", "");
});
};
const isInline = formStyles.formStyle === "inline";
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 (
<>
<form
onSubmit={handleSubmit}
style={{
display: "flex",
flexDirection: isInline ? "row" : "column",
alignItems: "center",
justifyContent: "center",
width: "100%",
}}
>
<input
type="text"
name="message"
placeholder={"Enter your message here"}
value={message}
onChange={(e) => setMessage(e.target.value)}
required={true}
style={{
color: formStyles.formFontColor,
fontFamily: `'${formStyles.formFont}', sans-serif`,
fontSize: `${formStyles.formFontSizePx}px`,
margin: isInline ? "0px 10px 0px 0px" : "0px 0px 10px",
width: "100%",
maxWidth: "300px",
minWidth: "100px",
background: "#FFFFFF",
border: "1px solid #D1D5DB",
boxSizing: "border-box",
boxShadow: "rgba(0, 0, 0, 0.05) 0px 1px 2px",
borderRadius: "6px",
padding: "8px 12px",
}}
/>
<input
type="text"
name="email"
placeholder={formStyles.placeholderText}
value={email}
onChange={(e) => setEmail(e.target.value)}
required={true}
style={{
color: formStyles.formFontColor,
fontFamily: `'${formStyles.formFont}', sans-serif`,
fontSize: `${formStyles.formFontSizePx}px`,
margin: isInline ? "0px 10px 0px 0px" : "0px 0px 10px",
width: "100%",
maxWidth: "300px",
minWidth: "100px",
background: "#FFFFFF",
border: "1px solid #D1D5DB",
boxSizing: "border-box",
boxShadow: "rgba(0, 0, 0, 0.05) 0px 1px 2px",
borderRadius: "6px",
padding: "8px 12px",
}}
/>
<SignUpFormButton />
</form>
</>
);
}
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 SignUpFormButton({ props }: any) {
return (
<button
type="submit"
style={{
background: formStyles.buttonColor,
fontSize: `${formStyles.buttonFontSizePx}px`,
color: formStyles.buttonFontColor,
fontFamily: `'${formStyles.buttonFont}', sans-serif`,
width: isInline ? "min-content" : "100%",
maxWidth: "300px",
whiteSpace: isInline ? "nowrap" : "normal",
height: "38px",
alignItems: "center",
justifyContent: "center",
flexDirection: "row",
padding: "9px 17px",
boxShadow: "0px 1px 2px rgba(0, 0, 0, 0.05)",
borderRadius: "6px",
textAlign: "center",
fontStyle: "normal",
fontWeight: 500,
lineHeight: "20px",
border: "none",
cursor: "pointer",
}}
>
{formState === SUBMITTING ? "Please wait..." : formStyles.buttonText}
</button>
);
}
}
function isValidEmail(email: any) {
return /.+@.+/.test(email);
}
@phil-loops

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment