diff --git a/web/package-lock.json b/web/package-lock.json index e179198b..a38af0fd 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8113,11 +8113,11 @@ "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -14491,9 +14491,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/multicast-dns": { "version": "7.2.5", @@ -18086,11 +18086,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/send/node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -26482,11 +26477,11 @@ "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "decamelize": { @@ -30845,9 +30840,9 @@ "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==" }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "multicast-dns": { "version": "7.2.5", @@ -33366,11 +33361,6 @@ } } }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", diff --git a/web/src/components/AdoptersPlanSignup/AdoptersPlanSignup.jsx b/web/src/components/AdoptersPlanSignup/AdoptersPlanSignup.jsx new file mode 100644 index 00000000..af232cd1 --- /dev/null +++ b/web/src/components/AdoptersPlanSignup/AdoptersPlanSignup.jsx @@ -0,0 +1,281 @@ +import { useState } from "react"; +import styles from "./index.module.css"; + +const INIT = "INIT"; +const SUBMITTING = "SUBMITTING"; +const ERROR = "ERROR"; +const SUCCESS = "SUCCESS"; +const formStates = [INIT, SUBMITTING, ERROR, SUCCESS]; +const formStyles = { + id: "clxnbxyqc0003ixgw637o04xb", + name: "Default", + placeholderText: "you@rock.dev", + formFont: "Inter", + formFontColor: "#fff", + formFontSizePx: 18, + buttonText: "Get your platform", + buttonFont: "Inter", + buttonFontColor: "#ffffff", + buttonFontSizePx: 22, + successMessage: "You rock! 🙌", + successFont: "Inter", + successFontColor: "#ffffff", + successFontSizePx: 20, + userGroup: "adopters plan", +}; +const domain = "app.loops.so"; + +export default function AdoptersPlanSignup() { + const [email, setEmail] = useState(""); + const [formState, setFormState] = useState(INIT); + const [errorMessage, setErrorMessage] = useState(""); + const [fields, setFields] = 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) => { + // 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 additional fields + const additionalFields = Object.entries(fields).reduce( + (acc, [key, val]) => { + if (val) { + return acc + "&" + key + "=" + encodeURIComponent(val); + } + return acc; + }, + "", + ); + + // 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 + additionalFields, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + .then((res) => [res.ok, res.json(), res]) + .then(([ok, dataPromise, res]) => { + if (ok) { + resetForm(); + setFormState(SUCCESS); + } else { + dataPromise.then((data) => { + 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 ( +
+

+ {formStyles.successMessage} +

+
+ ); + case ERROR: + return ( + <> + + + + ); + default: + return ( + <> +
+ 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 16px", + width: "80%", + minWidth: "100px", + background: "rgba(255, 255, 255, 0)", + border: "none", + borderBottom: "1px solid #D1D5DB", + boxSizing: "border-box", + padding: "8px 12px", + }} + /> + + + + + ); + } + + function SignUpFormError() { + return ( +
+

+ {errorMessage || "Oops! Something went wrong, please try again"} +

+
+ ); + } + + function BackButton() { + const [isHovered, setIsHovered] = useState(false); + + return ( + + ); + } + + function SignUpFormButton() { + return ( + + ); + } +} + +function isValidEmail(email) { + return /.+@.+/.test(email); +} diff --git a/web/src/components/AdoptersPlanSignup/index.module.css b/web/src/components/AdoptersPlanSignup/index.module.css new file mode 100644 index 00000000..1c0ea47c --- /dev/null +++ b/web/src/components/AdoptersPlanSignup/index.module.css @@ -0,0 +1,13 @@ +.submitButton { + background-color: #ff8803; +} + +.submitButton:hover { + background-color: #ffa229; + text-decoration: none; +} + +.adopterInput::placeholder { + color: #fff; + opacity: 1; /* Firefox */ +} diff --git a/web/src/pages/index.module.css b/web/src/pages/index.module.css index f86c5c40..550dad82 100644 --- a/web/src/pages/index.module.css +++ b/web/src/pages/index.module.css @@ -86,7 +86,7 @@ .pricingCard { backdrop-filter: blur(3px); - background-color: rgba(255, 255, 255, 0.3); + background-color: rgba(255, 255, 255, 0.2); width: 100%; height: 100%; padding: 20px; @@ -97,6 +97,20 @@ text-align: center; } +.pricingCardSupport { + backdrop-filter: blur(3px); + background-color: rgba(255, 255, 255, 0.37); + width: 100%; + height: 100%; + padding: 20px; + border-radius: 10px; + display: flex; + flex-direction: column; + text-align: center; + border: solid 4px #ff8803; + padding-bottom: 0; +} + .pricingButton { border: none; color: white; diff --git a/web/src/pages/pricing.js b/web/src/pages/pricing.js index 21e2e037..4f44d5b9 100644 --- a/web/src/pages/pricing.js +++ b/web/src/pages/pricing.js @@ -3,6 +3,7 @@ import { Card, Button, Typography, Row, Col, ConfigProvider } from "antd"; import CalendlyWidget from "../components/CalendlyWidget"; import styles from "./index.module.css"; import yaml from "/static/img/yaml_background.png"; +import AdoptersPlanSignup from "../components/AdoptersPlanSignup/AdoptersPlanSignup"; const { Paragraph } = Typography; @@ -98,13 +99,26 @@ export default function Pricing() { - +

We are Open Source!

+
- - + +

- Need onboarding? + Onboarding and support

+
+
+ Get support from our team to customize Cyclops to your use + case +
  • - Integrating Cyclops into your existing workflow + Setting up your Cyclops instance
  • +
  • Creating templates
  • - Creating custom templates for your use cases + Onboarding your developer teams
  • - Onboarding your developer teams + Support maintaining Cyclops through upgrades
-
-
- -
-
- - - -

- Looking for something more? -

- -
    -
  • - Don't want to host it on your own? -
  • -
  • - Interested in additional features? -
  • -
+ Leave your email, and our team will reach out to you in the + next 24 hours -{" "} + no commitment +
- +