Skip to content

Commit c022f84

Browse files
authored
✨ update pricing page (cyclops-ui#682)
* update pricing page * add husky * add husky * indent pricing * indent submit * remove husky * signup button margin
1 parent 751c43d commit c022f84

File tree

5 files changed

+373
-74
lines changed

5 files changed

+373
-74
lines changed

web/package-lock.json

+14-24
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import { useState } from "react";
2+
import styles from "./index.module.css";
3+
4+
const INIT = "INIT";
5+
const SUBMITTING = "SUBMITTING";
6+
const ERROR = "ERROR";
7+
const SUCCESS = "SUCCESS";
8+
const formStates = [INIT, SUBMITTING, ERROR, SUCCESS];
9+
const formStyles = {
10+
id: "clxnbxyqc0003ixgw637o04xb",
11+
name: "Default",
12+
placeholderText: "[email protected]",
13+
formFont: "Inter",
14+
formFontColor: "#fff",
15+
formFontSizePx: 18,
16+
buttonText: "Get your platform",
17+
buttonFont: "Inter",
18+
buttonFontColor: "#ffffff",
19+
buttonFontSizePx: 22,
20+
successMessage: "You rock! 🙌",
21+
successFont: "Inter",
22+
successFontColor: "#ffffff",
23+
successFontSizePx: 20,
24+
userGroup: "adopters plan",
25+
};
26+
const domain = "app.loops.so";
27+
28+
export default function AdoptersPlanSignup() {
29+
const [email, setEmail] = useState("");
30+
const [formState, setFormState] = useState(INIT);
31+
const [errorMessage, setErrorMessage] = useState("");
32+
const [fields, setFields] = useState({});
33+
34+
const resetForm = () => {
35+
setEmail("");
36+
setFormState(INIT);
37+
setErrorMessage("");
38+
};
39+
40+
/**
41+
* Rate limit the number of submissions allowed
42+
* @returns {boolean} true if the form has been successfully submitted in the past minute
43+
*/
44+
const hasRecentSubmission = () => {
45+
const time = new Date();
46+
const timestamp = time.valueOf();
47+
const previousTimestamp = localStorage.getItem("loops-form-timestamp");
48+
49+
// Indicate if the last sign up was less than a minute ago
50+
if (
51+
previousTimestamp &&
52+
Number(previousTimestamp) + 60 * 1000 > timestamp
53+
) {
54+
setFormState(ERROR);
55+
setErrorMessage("Too many signups, please try again in a little while");
56+
return true;
57+
}
58+
59+
localStorage.setItem("loops-form-timestamp", timestamp.toString());
60+
return false;
61+
};
62+
63+
const handleSubmit = (event) => {
64+
// Prevent the default form submission
65+
event.preventDefault();
66+
67+
// boundary conditions for submission
68+
if (formState !== INIT) return;
69+
if (!isValidEmail(email)) {
70+
setFormState(ERROR);
71+
setErrorMessage("Please enter a valid email");
72+
return;
73+
}
74+
if (hasRecentSubmission()) return;
75+
setFormState(SUBMITTING);
76+
77+
// build additional fields
78+
const additionalFields = Object.entries(fields).reduce(
79+
(acc, [key, val]) => {
80+
if (val) {
81+
return acc + "&" + key + "=" + encodeURIComponent(val);
82+
}
83+
return acc;
84+
},
85+
"",
86+
);
87+
88+
// build body
89+
const formBody = `userGroup=${encodeURIComponent(
90+
formStyles.userGroup,
91+
)}&email=${encodeURIComponent(email)}&mailingLists=`;
92+
93+
// API request to add user to newsletter
94+
fetch(`https://${domain}/api/newsletter-form/${formStyles.id}`, {
95+
method: "POST",
96+
body: formBody + additionalFields,
97+
headers: {
98+
"Content-Type": "application/x-www-form-urlencoded",
99+
},
100+
})
101+
.then((res) => [res.ok, res.json(), res])
102+
.then(([ok, dataPromise, res]) => {
103+
if (ok) {
104+
resetForm();
105+
setFormState(SUCCESS);
106+
} else {
107+
dataPromise.then((data) => {
108+
setFormState(ERROR);
109+
setErrorMessage(data.message || res.statusText);
110+
localStorage.setItem("loops-form-timestamp", "");
111+
});
112+
}
113+
})
114+
.catch((error) => {
115+
setFormState(ERROR);
116+
// check for cloudflare error
117+
if (error.message === "Failed to fetch") {
118+
setErrorMessage(
119+
"Too many signups, please try again in a little while",
120+
);
121+
} else if (error.message) {
122+
setErrorMessage(error.message);
123+
}
124+
localStorage.setItem("loops-form-timestamp", "");
125+
});
126+
};
127+
128+
const isInline = formStyles.formStyle === "inline";
129+
130+
switch (formState) {
131+
case SUCCESS:
132+
return (
133+
<div
134+
style={{
135+
display: "flex",
136+
alignItems: "center",
137+
justifyContent: "center",
138+
width: "100%",
139+
}}
140+
>
141+
<p
142+
style={{
143+
fontFamily: `'${formStyles.successFont}', sans-serif`,
144+
color: formStyles.successFontColor,
145+
fontSize: `${formStyles.successFontSizePx}px`,
146+
}}
147+
>
148+
{formStyles.successMessage}
149+
</p>
150+
</div>
151+
);
152+
case ERROR:
153+
return (
154+
<>
155+
<SignUpFormError />
156+
<BackButton />
157+
</>
158+
);
159+
default:
160+
return (
161+
<>
162+
<form
163+
onSubmit={handleSubmit}
164+
style={{
165+
display: "flex",
166+
flexDirection: isInline ? "row" : "column",
167+
alignItems: "center",
168+
justifyContent: "center",
169+
width: "100%",
170+
}}
171+
>
172+
<input
173+
type="text"
174+
name="email"
175+
placeholder={formStyles.placeholderText}
176+
value={email}
177+
className={styles.adopterInput}
178+
onChange={(e) => setEmail(e.target.value)}
179+
required={true}
180+
style={{
181+
color: formStyles.formFontColor,
182+
fontFamily: `'${formStyles.formFont}', sans-serif`,
183+
fontSize: `${formStyles.formFontSizePx}px`,
184+
margin: isInline ? "0px 10px 0px 0px" : "0px 0px 16px",
185+
width: "80%",
186+
minWidth: "100px",
187+
background: "rgba(255, 255, 255, 0)",
188+
border: "none",
189+
borderBottom: "1px solid #D1D5DB",
190+
boxSizing: "border-box",
191+
padding: "8px 12px",
192+
}}
193+
/>
194+
<div aria-hidden="true" style={{ position: "absolute" }}></div>
195+
<SignUpFormButton />
196+
</form>
197+
</>
198+
);
199+
}
200+
201+
function SignUpFormError() {
202+
return (
203+
<div
204+
style={{
205+
alignItems: "center",
206+
justifyContent: "center",
207+
width: "100%",
208+
}}
209+
>
210+
<p
211+
style={{
212+
fontFamily: "Inter, sans-serif",
213+
color: "rgb(185, 28, 28)",
214+
fontSize: "14px",
215+
}}
216+
>
217+
{errorMessage || "Oops! Something went wrong, please try again"}
218+
</p>
219+
</div>
220+
);
221+
}
222+
223+
function BackButton() {
224+
const [isHovered, setIsHovered] = useState(false);
225+
226+
return (
227+
<button
228+
style={{
229+
color: "#6b7280",
230+
font: "14px, Inter, sans-serif",
231+
margin: "10px auto",
232+
textAlign: "center",
233+
background: "transparent",
234+
border: "none",
235+
cursor: "pointer",
236+
textDecoration: isHovered ? "underline" : "none",
237+
}}
238+
onMouseOut={() => setIsHovered(false)}
239+
onMouseOver={() => setIsHovered(true)}
240+
onClick={resetForm}
241+
>
242+
&larr; Back
243+
</button>
244+
);
245+
}
246+
247+
function SignUpFormButton() {
248+
return (
249+
<button
250+
type="submit"
251+
className={styles.submitButton}
252+
style={{
253+
fontSize: `${formStyles.buttonFontSizePx}px`,
254+
color: formStyles.buttonFontColor,
255+
fontFamily: `'${formStyles.buttonFont}', sans-serif`,
256+
maxWidth: "300px",
257+
whiteSpace: isInline ? "nowrap" : "normal",
258+
height: "40px",
259+
alignItems: "center",
260+
justifyContent: "center",
261+
flexDirection: "row",
262+
padding: "9px 17px",
263+
boxShadow: "0px 1px 2px rgba(0, 0, 0, 0.05)",
264+
borderRadius: "40px",
265+
textAlign: "center",
266+
fontStyle: "normal",
267+
fontWeight: 700,
268+
lineHeight: "20px",
269+
border: "none",
270+
cursor: "pointer",
271+
}}
272+
>
273+
{formState === SUBMITTING ? "Please wait..." : formStyles.buttonText}
274+
</button>
275+
);
276+
}
277+
}
278+
279+
function isValidEmail(email) {
280+
return /.+@.+/.test(email);
281+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.submitButton {
2+
background-color: #ff8803;
3+
}
4+
5+
.submitButton:hover {
6+
background-color: #ffa229;
7+
text-decoration: none;
8+
}
9+
10+
.adopterInput::placeholder {
11+
color: #fff;
12+
opacity: 1; /* Firefox */
13+
}

0 commit comments

Comments
 (0)