diff --git a/_app_redirects b/_app_redirects new file mode 100644 index 0000000..cf91099 --- /dev/null +++ b/_app_redirects @@ -0,0 +1,7 @@ +# This template uses this file instead of the typicial Netlify _redirects file. +# For more information about redirects and rewrites, see https://docs.netlify.com/routing/redirects/. + +# Do not remove the line below. This is required to serve the site when deployed. +/* /.netlify/functions/server 200 + +# Add other redirects and rewrites here and/or in your netlify.toml diff --git a/app/components/formInput.tsx b/app/components/formInput.tsx new file mode 100644 index 0000000..60f3255 --- /dev/null +++ b/app/components/formInput.tsx @@ -0,0 +1,34 @@ +interface FormInputProps { + type: string, + name: string, + id: string, + image: string, + label: string, + value: string | number, + handleInputChange: Function, + required?: boolean, + min?: string +} + +const FormInput = ({ type, name, id, image, label, value, handleInputChange, required = false, min} : FormInputProps) => { + return ( +
+ {image && } + +
+ ) +} + +export default FormInput \ No newline at end of file diff --git a/app/components/toggle.tsx b/app/components/toggle.tsx new file mode 100644 index 0000000..a710535 --- /dev/null +++ b/app/components/toggle.tsx @@ -0,0 +1,23 @@ +import { useState } from 'react'; + +interface ToggleProps { + onChange: Function +} + +const Toggle = ({ onChange } : ToggleProps) => { + const [isToggled, setToggle] = useState(false) + + const handleChange = (event : React.FormEvent) => { + onChange(event); + setToggle(!isToggled) + } + + return ( +
+ + +
+ ) +} + +export default Toggle \ No newline at end of file diff --git a/app/root.tsx b/app/root.tsx index 81181cc..201102b 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -8,11 +8,16 @@ import { ScrollRestoration, } from "@remix-run/react"; -import stylesheet from "~/tailwind.css"; - +import stylesheet from "./styles/tailwind.css"; +import styles from "./styles/app.css" +import toggleStyles from "./styles/toggle.css" export const links: LinksFunction = () => [ { rel: "stylesheet", href: stylesheet }, + { rel: "preconnect", href: "https://fonts.googleapis.com", crossOrigin: "anonymous"}, + { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@300;400;600;900&display=swap", crossOrigin: "anonymous"}, + { rel: "stylesheet", href: styles}, + { rel: "stylesheet", href: toggleStyles} ]; export default function App() { diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 64163f8..05699d2 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,23 +1,175 @@ +import { useState } from "react"; import type { V2_MetaFunction } from "@remix-run/node"; +import Toggle from "../components/toggle"; +import FormInput from "../components/formInput" export const meta: V2_MetaFunction = () => { return [ - { title: "New Remix App" }, - { name: "description", content: "Welcome to Remix!" }, + { title: "Cablecast Captioning Calculator" }, + { + name: "description", + content: + "Calculate how many captioning minutes you will need for a year of programming!", + }, ]; }; export default function Index() { + const [formState, setFormState] = useState({ + name: "", + email: "", + averageProgramsPerMonth: 0, + averageLengthOfProgramsInHours: 0, + needsTranslations: false, + }); + const [hasFormSubmit, setHasFormSubmit] = useState(false); + const [captionMinutes, setCaptionMinutes] = useState(0); + const [hasError, setHasError] = useState(false); + + const handleInputChange = (e: React.FormEvent) => { + const { name, value } = e.target as HTMLInputElement; + console.log(name, value); + setFormState((prevProps) => ({ + ...prevProps, + [name]: value, + })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!validateInput()) { + setHasError(true); + } else { + try { + fetch("/api/leads", { + method: "POST", + body: JSON.stringify(formState), + headers: { + "Content-Type": "application/json", + }, + }); + } catch (error) { + console.log(error); + } + setHasFormSubmit(true); + calcCaptionMinutes(); + } + }; + + const validateInput = () => { + if (formState.name === "" || formState.name.length < 2) { + return false; + } else if (formState.email === "" || !formState.email.includes("@")) { + return false; + } else { + return true; + } + }; + + const calcCaptionMinutes = () => { + let captioningMinutes = + formState.averageProgramsPerMonth * + formState.averageLengthOfProgramsInHours * + 60; + setCaptionMinutes(captioningMinutes); + }; + return ( -
-

Hello world!

- {/* - // Use the form below if you wan to go remix style, or just use fetch in an event handler. - */} -
- - -
+
+

+ Captioning Calculator +

+
+
+

+ How many captioning minutes will you need? +

+

+ The Cablecast Captioning Calculator is a tool to understand how many + Cablecast Captioning Minutes you will need for a year of + programming. +

+
+
+ +
+
+
+ {hasFormSubmit ? ( + <> +

+ You will need approximately +

+
+

{captionMinutes} minutes

+
+

+ {" "} + Of Closed Captioning for 1 year +

+ + ) : ( +
+ + + + +
+ + +
+
+ {hasError && ( // if hasError is true, then render the error message +

There was an error in your submission. Please ensure your name has at least 2 characters and your email contains an '@'.

+ )} + +
+ + )} +
); } diff --git a/app/styles/app.css b/app/styles/app.css new file mode 100644 index 0000000..2acbfea --- /dev/null +++ b/app/styles/app.css @@ -0,0 +1,90 @@ +body { + color: #545c6f; + font-family: Nunito Sans,sans-serif; + font-size: .875rem; + line-height: 1.5; + font-weight: 600; +} + +body:before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 10px; + background: #2cad66; +} + +h1 { + font-weight: 400; + font-size: 6rem; +} + +h2 { + font-weight: 600; +} + +.decorative-img { + max-height: 300px; + margin-left: auto; + margin-right: auto; +} + +.app-container { + max-width: 1224px; +} + +.form-container { + background: #EDEEF1; +} + +.form-container form { + max-width: 900px; + margin-left: auto; + margin-right: auto; +} + +.form-container form > div { + margin-top: 20px; +} + +.form-container div > img { + width: 70px; +} + +@media screen and (min-width: 767px) { + .form-container div > img { + margin-right: 10px; + } +} + +.form-container input, +.minutes-needed { + outline: none; + border: solid #8e97aa 3px; + padding-left: 10px; + margin-left: 20px; + margin-right: 20px; +} + +.minutes-needed { + text-align: center; + margin: 3rem auto; + border-radius: 25px; + font-size: 3rem; + border-radius: 50px; + background: white; + border-width: 5px; + padding-left: 20px; + padding-right: 20px; +} + +.form-container input:focus { + border: #2cad66 solid 3px; +} + +.form-container .btn.green { + background: #2cad66; + color: white; +} \ No newline at end of file diff --git a/app/tailwind.css b/app/styles/tailwind.css similarity index 100% rename from app/tailwind.css rename to app/styles/tailwind.css diff --git a/app/styles/toggle.css b/app/styles/toggle.css new file mode 100644 index 0000000..b25434c --- /dev/null +++ b/app/styles/toggle.css @@ -0,0 +1,50 @@ +.toggle-btn { + position: relative; + display: inline-block; + width: 65px; + height: 35px; +} + +.toggle-btn input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-btn span { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #2c3e50; + transition: 0.3s; + border-radius: 30px; +} + +.toggle-btn span:before { + position: absolute; + content: ""; + height: 28px; + width: 28px; + left: 3px; + bottom: 3.6px; + background-color: #fff; + border-radius: 50%; + transition: 0.3s; +} + +.toggle-btn input:checked + span { + background-color: #2cad66; +} + +.toggle-btn input:checked + span:before { + transform: translateX(30px); +} + +@media screen and (min-width: 767px) { + .toggle-btn { + margin-left: 20px; + } +} \ No newline at end of file diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..630bab8 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,14 @@ +[build] +command = "remix build && cp _app_redirects public/_redirects" +publish = "public" + +[dev] +command = "npm run dev" +targetPort = 3000 + +[[headers]] +for = "/build/*" + +[headers.values] +# Set to 60 seconds as an example. You can also add cache headers via Remix. See the documentation on [headers](https://remix.run/docs/en/v1/route/headers) in Remix. +"Cache-Control" = "public, max-age=60, s-maxage=60" diff --git a/package-lock.json b/package-lock.json index ff8abab..b63e246 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "dependencies": { "@remix-run/css-bundle": "^1.17.1", + "@remix-run/netlify": "^1.18.0", "@remix-run/node": "^1.17.1", "@remix-run/react": "^1.17.1", "@remix-run/serve": "^1.17.1", @@ -2639,6 +2640,18 @@ "integrity": "sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==", "dev": true }, + "node_modules/@netlify/functions": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-1.6.0.tgz", + "integrity": "sha512-6G92AlcpFrQG72XU8YH8pg94eDnq7+Q0YJhb8x4qNpdGsvuzvrfHWBmqFGp/Yshmv4wex9lpsTRZOocdrA2erQ==", + "peer": true, + "dependencies": { + "is-promise": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -2886,6 +2899,63 @@ "express": "^4.17.1" } }, + "node_modules/@remix-run/netlify": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@remix-run/netlify/-/netlify-1.18.0.tgz", + "integrity": "sha512-nsCL5cg8Y1dPuQkfNrkZOvCue1wrRGLvyBBHGAdlirt1Hq1pfcwzCtnEManrG7CjW5a73DjvYepLPn6EWb3doA==", + "dependencies": { + "@remix-run/node": "1.18.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@netlify/functions": "^0.10.0 || ^0.11.0 || ^1.0.0" + } + }, + "node_modules/@remix-run/netlify/node_modules/@remix-run/node": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-1.18.0.tgz", + "integrity": "sha512-RRsGS+19tpYj2EXid8PP+bV8Zj6P/4yT6pMkRCIlw1uY0uRUiA3PocnyGmgl6TsqsqaahC7KdDAxhVayt4tdZA==", + "dependencies": { + "@remix-run/server-runtime": "1.18.0", + "@remix-run/web-fetch": "^4.3.4", + "@remix-run/web-file": "^3.0.2", + "@remix-run/web-stream": "^1.0.3", + "@web3-storage/multipart-parser": "^1.0.0", + "abort-controller": "^3.0.0", + "cookie-signature": "^1.1.0", + "source-map-support": "^0.5.21", + "stream-slice": "^0.1.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@remix-run/netlify/node_modules/@remix-run/router": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.0.tgz", + "integrity": "sha512-Eu1V3kz3mV0wUpVTiFHuaT8UD1gj/0VnoFHQYX35xlslQUpe8CuYoKFn9d4WZFHm3yDywz6ALZuGdnUPKrNeAw==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@remix-run/netlify/node_modules/@remix-run/server-runtime": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-1.18.0.tgz", + "integrity": "sha512-iiSKgGIWMkvf4ftnjGBmIJpgqxRwv8XQilAINapaYsx1zEM6egZGYE6WvaxLuRQSceZZNgLAYzL48TmK+DAU5g==", + "dependencies": { + "@remix-run/router": "1.7.0", + "@types/cookie": "^0.4.1", + "@web3-storage/multipart-parser": "^1.0.0", + "cookie": "^0.4.1", + "set-cookie-parser": "^2.4.8", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@remix-run/node": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-1.17.1.tgz", @@ -3116,6 +3186,11 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "node_modules/@types/debug": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", @@ -7944,6 +8019,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "peer": true + }, "node_modules/is-reference": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.1.tgz", diff --git a/package.json b/package.json index 376c099..fc8115a 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,12 @@ "build": "remix build", "deploy": "fly deploy --remote-only", "dev": "remix dev", - "start": "remix-serve build", + "start": "netlify serve", "typecheck": "tsc" }, "dependencies": { "@remix-run/css-bundle": "^1.17.1", + "@remix-run/netlify": "^1.18.0", "@remix-run/node": "^1.17.1", "@remix-run/react": "^1.17.1", "@remix-run/serve": "^1.17.1", diff --git a/public/cc-decorative-img.png b/public/cc-decorative-img.png new file mode 100644 index 0000000..90c3308 Binary files /dev/null and b/public/cc-decorative-img.png differ diff --git a/public/clock.png b/public/clock.png new file mode 100644 index 0000000..15235ec Binary files /dev/null and b/public/clock.png differ diff --git a/public/email.png b/public/email.png new file mode 100644 index 0000000..80545de Binary files /dev/null and b/public/email.png differ diff --git a/public/favicon.ico b/public/favicon.ico index 8830cf6..4b09a39 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/name.png b/public/name.png new file mode 100644 index 0000000..b38b663 Binary files /dev/null and b/public/name.png differ diff --git a/public/number.png b/public/number.png new file mode 100644 index 0000000..3aecc8d Binary files /dev/null and b/public/number.png differ diff --git a/public/translate.png b/public/translate.png new file mode 100644 index 0000000..bfcbae0 Binary files /dev/null and b/public/translate.png differ diff --git a/remix.config.js b/remix.config.js index 3f82c03..9dc6353 100644 --- a/remix.config.js +++ b/remix.config.js @@ -1,5 +1,16 @@ +const baseConfig = + process.env.NODE_ENV === "production" + ? // when running the Netify CLI or building on Netlify, we want to use + { + server: "./server.js", + serverBuildPath: ".netlify/functions-internal/server.js", + } + : // otherwise support running remix dev, i.e. no custom server + undefined; + /** @type {import('@remix-run/dev').AppConfig} */ module.exports = { + ...baseConfig, tailwind: true, ignoredRouteFiles: ["**/.*"], // appDirectory: "app", diff --git a/server.js b/server.js new file mode 100644 index 0000000..b17fe56 --- /dev/null +++ b/server.js @@ -0,0 +1,7 @@ +import { createRequestHandler } from "@remix-run/netlify"; +import * as build from "@remix-run/dev/server-build"; + +export const handler = createRequestHandler({ + build, + mode: process.env.NODE_ENV, +});