Skip to content

Captioning calculator #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions app/components/calculator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { ValidatedForm } from "remix-validated-form";
import { withZod } from "@remix-validated-form/with-zod";
import { z } from "zod";
import { useIsSubmitting } from "remix-validated-form";
import { useField } from "remix-validated-form";
import { useState, useEffect } from "react";

/*------------------------------------------
HELPER FUNCTIONS
------------------------------------------ */
// NOTE: I decided to try out remix-validated-form because of it's simplicity and ease for validation. I originally used 'useActionData();' to do server-side validation, but ran into some issues due to returning a JSON response instead of a redirect, which caused the app to stay on the action's route.

const validator = withZod(
z.object({
name: z.string().min(1, { message: "First name is required" }),
email: z.string().min(1, { message: "Email is required" }).email("Must be a valid email"),
averageProgramsPerMonth: z.string().min(1, { message: "Average Number of Programs Per Month is required" }),
averageLengthOfProgramsInHours: z
.string()
.min(1, { message: "Average Length Of Programs In Hours is required" }),
})
);

/*------------------------------------------
JSX ELEMENTS
------------------------------------------ */
const SubmitButton = () => {
const isSubmitting = useIsSubmitting();
return (
<button type="submit" disabled={isSubmitting} className="rounded-full bg-logo-green text-white p-2 px-4">
{isSubmitting ? "CALCULATING..." : "CALCULATE"}
</button>
);
};

type TextInputProps = {
name: string;
label: string;
type: string;
};

// NOTE: remix-validated-form helped me to reduce my code quite a bit by being able to reuse a single component for both inputs and error handling. It was easy to customize the styles in one place as well.
const TextInput = ({ name, label, type }: TextInputProps) => {
const { error, getInputProps } = useField(name);
const [borderColor, setBorderColor] = useState("medium-grey");

useEffect(() => {
error ? setBorderColor("red-500") : setBorderColor("medium-grey");
}, [error]);

return (
<div className="sm:flex sm:grow">
<label className="sm:mr-4 flex items-center sm:flex-nowrap" htmlFor={name}>
{label}
</label>
<input
className={`rounded-3xl border-4 border-${borderColor} py-px pl-1.5 text-center w-full max-w-sm`}
{...getInputProps({
id: name,
type: type,
})}
/>
{error && <div className="text-red-500 text-sm pl-2">*{error}</div>}
</div>
);
};

export default function Calculator() {
// NOTE: I wasn't sure if you wanted the calculation to occur in the backend or frontend. I chose the frontend since it wasn't a complex function, and didn't slow down the the user's experience.
const [yearlyCaptionMins, setYearlyCaptionMins] = useState(0);

const calculateCaptions = (form) => {
const averageProgramsPerMonth = form.averageProgramsPerMonth;
const averageLengthOfProgramsInHours = form.averageLengthOfProgramsInHours;
setYearlyCaptionMins(averageProgramsPerMonth * averageLengthOfProgramsInHours * 60);
};

const CalculatorForm = () => {
return (
<ValidatedForm
validator={validator}
action="/api/leads"
method="POST"
className="text-2xl"
onSubmit={calculateCaptions}
>
<div className="flex justify-start items-center flex-wrap sm:flex-nowrap">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="#26A35C"
className="w-10 h-10 mr-4 hidden sm:block"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<TextInput name="name" label="Name:" type="text" />
</div>
<div className="flex justify-start items-center flex-wrap sm:flex-nowrap my-6">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="#26A35C"
className="w-10 h-10 mr-4 hidden sm:block"
>
<path
strokeLinecap="round"
d="M16.5 12a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zm0 0c0 1.657 1.007 3 2.25 3S21 13.657 21 12a9 9 0 10-2.636 6.364M16.5 12V8.25"
/>
</svg>
<TextInput name="email" label="Email:" type="text" />
</div>
<div className="flex justify-start items-center flex-wrap sm:flex-nowrap my-6">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="#26A35C"
className="w-10 h-10 mr-4 hidden sm:block"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5l-3.9 19.5m-2.1-19.5l-3.9 19.5"
/>
</svg>
<TextInput
name="averageProgramsPerMonth"
label="Average Number of Programs Per Month:"
type="number"
/>
</div>
<div className="flex justify-start items-center flex-wrap sm:flex-nowrap my-6">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="#26A35C"
className="w-10 h-10 mr-4 hidden sm:block"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<TextInput
name="averageLengthOfProgramsInHours"
label="Average Length Of Programs In Hours:"
type="number"
/>
</div>
<div className="flex justify-start items-center flex-wrap sm:flex-nowrap my-6">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="#26A35C"
className="w-10 h-10 mr-4 hidden sm:block"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418"
/>
</svg>
<span className="ml-3 mr-4 text-gray-900 dark:text-gray-300">Do You Need Translations?</span>
<label className="relative inline-flex items-center cursor-pointer my-0 mx-auto sm:ml-0">
<input
type="checkbox"
className="sr-only peer"
id="needsTranslations"
name="needsTranslations"
/>
<div className="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-1 peer-focus:ring-logo-green dark:peer-focus:ring-logo-green dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-logo-green"></div>
</label>
</div>
<div className="text-center">
<SubmitButton />
</div>
</ValidatedForm>
);
};
const CalculatorResults = () => {
return (
<>
<h1 className="text-2xl text-center">You will need approximately</h1>
<div className="text-center my-8">
<input
id="closedCaptioning"
className="rounded-3xl border-4 border-medium-grey py-2 pl-1.5 text-center bg-white"
value={yearlyCaptionMins}
readOnly={true}
disabled={true}
/>
</div>
<h2 className="text-2xl text-center">Of Closed Captioning Minutes for 1 year</h2>
</>
);
};

return (
<div className="bg-light-grey container mx-auto rounded-3xl lg:w-7/12 w-11/12 p-10 lg:pl-14 pl-4 mt-6 lg:mb-10">
{yearlyCaptionMins === 0 ? <CalculatorForm /> : <CalculatorResults />}
</div>
);
}
88 changes: 88 additions & 0 deletions app/components/navigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import logo from "../../public/cablecast-logo.png";
import { useState } from "react";

export default function Navigation() {
const [navbar, setNavbar] = useState(false);
return (
<nav className="flex items-center md:justify-between justify-center flex-wrap bg-dark-grey p-6 lg:px-64 text-xl border-b-8 border-logo-green">
<img src={logo} className="mx-10 sm:mx-0" />
<div className="md:hidden flex items-center">
<button className="outline-none" onClick={() => setNavbar(!navbar)}>
<svg
className="w-6 h-6 text-gray-500"
x-show="! showMenu"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 00 24 24"
stroke="white"
>
<path d="m4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
<div>
<a
href="/"
className={`block mt-4 md:inline-block md:mt-0 text-white mr-4 ${navbar ? "block" : "hidden"}`}
>
Products
</a>
<a
href="/"
className={`block mt-4 md:inline-block md:mt-0 text-white mr-4 ${navbar ? "block" : "hidden"}`}
>
Services & Add Ons
</a>
<a
href="/"
className={`block mt-4 md:inline-block md:mt-0 text-white mr-4 ${navbar ? "block" : "hidden"}`}
>
Support
</a>
<a
href="/"
className={`block mt-4 md:inline-block md:mt-0 text-white mr-4 ${navbar ? "block" : "hidden"}`}
>
News & Resources
</a>
<a
href="/"
className={`block mt-4 md:inline-block md:mt-0 text-white mr-4 ${navbar ? "block" : "hidden"}`}
>
About Us
</a>
</div>
<div className="md:flex hidden">
{/* Twitter */}
<a href="/">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mx-2" fill="white" viewBox="0 0 24 24">
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z" />
</svg>
</a>
{/* Facebook */}
<a href="/">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mx-2" fill="white" viewBox="0 0 24 24">
<path d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z" />
</svg>
</a>
{/* Linkedin */}
<a href="/">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mx-2" fill="white" viewBox="0 0 24 24">
<path d="M4.98 3.5c0 1.381-1.11 2.5-2.48 2.5s-2.48-1.119-2.48-2.5c0-1.38 1.11-2.5 2.48-2.5s2.48 1.12 2.48 2.5zm.02 4.5h-5v16h5v-16zm7.982 0h-4.968v16h4.969v-8.399c0-4.67 6.029-5.052 6.029 0v8.399h4.988v-10.131c0-7.88-8.922-7.593-11.018-3.714v-2.155z" />
</svg>
</a>
</div>
<div>
<a
href="#"
className="lg:block hidden
text-base p-4 leading-none border border-cta-blue rounded text-white bg-cta-blue mt-4 md:mt-0 font-bold"
>
GET IN TOUCH
</a>
</div>
</nav>
);
}
47 changes: 21 additions & 26 deletions app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,30 @@
import type { LinksFunction } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";

import stylesheet from "~/tailwind.css";


export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet },
{ rel: "stylesheet", href: stylesheet },
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{ rel: "preconnect", href: "https://fonts.gstatic.com" },
{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@300&display=swap" },
];

export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
Loading